diff options
Diffstat (limited to 'drivers/power')
142 files changed, 82653 insertions, 0 deletions
diff --git a/drivers/power/Kconfig b/drivers/power/Kconfig new file mode 100644 index 000000000..696bf77a7 --- /dev/null +++ b/drivers/power/Kconfig @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +source "drivers/power/reset/Kconfig" +source "drivers/power/supply/Kconfig" diff --git a/drivers/power/Makefile b/drivers/power/Makefile new file mode 100644 index 000000000..effbf0377 --- /dev/null +++ b/drivers/power/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_POWER_RESET) += reset/ +obj-$(CONFIG_POWER_SUPPLY) += supply/ diff --git a/drivers/power/reset/Kconfig b/drivers/power/reset/Kconfig new file mode 100644 index 000000000..d55b3727e --- /dev/null +++ b/drivers/power/reset/Kconfig @@ -0,0 +1,288 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig POWER_RESET + bool "Board level reset or power off" + help + Provides a number of drivers which either reset a complete board + or shut it down, by manipulating the main power supply on the board. + + Say Y here to enable board reset and power off + +if POWER_RESET + +config POWER_RESET_AS3722 + bool "ams AS3722 power-off driver" + depends on MFD_AS3722 + help + This driver supports turning off board via a ams AS3722 power-off. + +config POWER_RESET_AT91_POWEROFF + tristate "Atmel AT91 poweroff driver" + depends on ARCH_AT91 + default SOC_AT91SAM9 || SOC_SAMA5 + help + This driver supports poweroff for Atmel AT91SAM9 and SAMA5 + SoCs + +config POWER_RESET_AT91_RESET + tristate "Atmel AT91 reset driver" + depends on ARCH_AT91 + default SOC_AT91SAM9 || SOC_SAM9X60 || SOC_SAMA5 + help + This driver supports restart for Atmel AT91SAM9 and SAMA5 + SoCs + +config POWER_RESET_AT91_SAMA5D2_SHDWC + tristate "Atmel AT91 SAMA5D2-Compatible shutdown controller driver" + depends on ARCH_AT91 + default SOC_SAM9X60 || SOC_SAMA5 + help + This driver supports the alternate shutdown controller for some Atmel + SAMA5 SoCs. It is present for example on SAMA5D2 SoC. + +config POWER_RESET_AXXIA + bool "LSI Axxia reset driver" + depends on ARCH_AXXIA + help + This driver supports restart for Axxia SoC. + + Say Y if you have an Axxia family SoC. + +config POWER_RESET_BRCMKONA + bool "Broadcom Kona reset driver" + depends on ARM || COMPILE_TEST + default ARCH_BCM_MOBILE + help + This driver provides restart support for Broadcom Kona chips. + + Say Y here if you have a Broadcom Kona-based board and you wish + to have restart support. + +config POWER_RESET_BRCMSTB + bool "Broadcom STB reset driver" + depends on ARM || ARM64 || MIPS || COMPILE_TEST + depends on MFD_SYSCON + default ARCH_BRCMSTB || BMIPS_GENERIC + help + This driver provides restart support for Broadcom STB boards. + + Say Y here if you have a Broadcom STB board and you wish + to have restart support. + +config POWER_RESET_GEMINI_POWEROFF + bool "Cortina Gemini power-off driver" + depends on ARCH_GEMINI || COMPILE_TEST + depends on OF && HAS_IOMEM + default ARCH_GEMINI + help + This driver supports turning off the Cortina Gemini SoC. + Select this if you're building a kernel with Gemini SoC support. + +config POWER_RESET_GPIO + bool "GPIO power-off driver" + depends on OF_GPIO + help + This driver supports turning off your board via a GPIO line. + If your board needs a GPIO high/low to power down, say Y and + create a binding in your devicetree. + +config POWER_RESET_GPIO_RESTART + bool "GPIO restart driver" + depends on OF_GPIO + help + This driver supports restarting your board via a GPIO line. + If your board needs a GPIO high/low to restart, say Y and + create a binding in your devicetree. + +config POWER_RESET_HISI + bool "Hisilicon power-off driver" + depends on ARCH_HISI + help + Reboot support for Hisilicon boards. + +config POWER_RESET_LINKSTATION + tristate "Buffalo LinkStation power-off driver" + depends on ARCH_MVEBU || COMPILE_TEST + depends on OF_MDIO && PHYLIB + help + This driver supports turning off some Buffalo LinkStations by + setting an output pin at the ethernet PHY to the correct state. + It also makes the device compatible with the WoL function. + + Say Y here if you have a Buffalo LinkStation LS421D/E. + +config POWER_RESET_MSM + bool "Qualcomm MSM power-off driver" + depends on ARCH_QCOM + help + Power off and restart support for Qualcomm boards. + +config POWER_RESET_QCOM_PON + tristate "Qualcomm power-on driver" + depends on ARCH_QCOM + depends on MFD_SPMI_PMIC + select REBOOT_MODE + help + Power On support for Qualcomm boards. + If you have a Qualcomm platform and need support for + power-on and reboot reason, Say Y. + If unsure, Say N. + +config POWER_RESET_OCELOT_RESET + bool "Microsemi Ocelot reset driver" + depends on MSCC_OCELOT || ARCH_SPARX5 || COMPILE_TEST + select MFD_SYSCON + help + This driver supports restart for Microsemi Ocelot SoC and similar. + +config POWER_RESET_OXNAS + bool "OXNAS SoC restart driver" + depends on ARCH_OXNAS + default MACH_OX820 + help + Restart support for OXNAS/PLXTECH OX820 SoC. + +config POWER_RESET_PIIX4_POWEROFF + tristate "Intel PIIX4 power-off driver" + depends on PCI + depends on MIPS || COMPILE_TEST + help + This driver supports powering off a system using the Intel PIIX4 + southbridge, for example the MIPS Malta development board. The + southbridge SOff state is entered in response to a request to + power off the system. + +config POWER_RESET_LTC2952 + bool "LTC2952 PowerPath power-off driver" + depends on OF_GPIO + help + This driver supports an external powerdown trigger and board power + down via the LTC2952. Bindings are made in the device tree. + +config POWER_RESET_MT6323 + bool "MediaTek MT6323 power-off driver" + depends on MFD_MT6397 + help + The power-off driver is responsible for externally shutdown down + the power of a remote MediaTek SoC MT6323 is connected to through + controlling a tiny circuit BBPU inside MT6323 RTC. + + Say Y if you have a board where MT6323 could be found. + +config POWER_RESET_QNAP + bool "QNAP power-off driver" + depends on OF_GPIO && PLAT_ORION + help + This driver supports turning off QNAP NAS devices by sending + commands to the microcontroller which controls the main power. + + Say Y if you have a QNAP NAS. + +config POWER_RESET_RESTART + bool "Restart power-off driver" + help + Some boards don't actually have the ability to power off. + Instead they restart, and u-boot holds the SoC until the + user presses a key. u-boot then boots into Linux. + +config POWER_RESET_ST + bool "ST restart driver" + depends on ARCH_STI + help + Reset support for STMicroelectronics boards. + +config POWER_RESET_VERSATILE + bool "ARM Versatile family reboot driver" + depends on ARM + depends on MFD_SYSCON + depends on OF + help + Power off and restart support for ARM Versatile family of + reference boards. + +config POWER_RESET_VEXPRESS + bool "ARM Versatile Express power-off and reset driver" + depends on ARM || ARM64 + depends on VEXPRESS_CONFIG=y + help + Power off and reset support for the ARM Ltd. Versatile + Express boards. + +config POWER_RESET_XGENE + bool "APM SoC X-Gene reset driver" + depends on ARM64 + help + Reboot support for the APM SoC X-Gene Eval boards. + +config POWER_RESET_KEYSTONE + bool "Keystone reset driver" + depends on ARCH_KEYSTONE || COMPILE_TEST + depends on HAS_IOMEM + select MFD_SYSCON + help + Reboot support for the KEYSTONE SoCs. + +config POWER_RESET_SYSCON + bool "Generic SYSCON regmap reset driver" + depends on OF + depends on HAS_IOMEM + select MFD_SYSCON + help + Reboot support for generic SYSCON mapped register reset. + +config POWER_RESET_SYSCON_POWEROFF + bool "Generic SYSCON regmap poweroff driver" + depends on OF + depends on HAS_IOMEM + select MFD_SYSCON + help + Poweroff support for generic SYSCON mapped register poweroff. + +config POWER_RESET_RMOBILE + tristate "Renesas R-Mobile reset driver" + depends on ARCH_RMOBILE || COMPILE_TEST + depends on HAS_IOMEM + help + Reboot support for Renesas R-Mobile and SH-Mobile SoCs. + +config POWER_RESET_ZX + tristate "ZTE SoCs reset driver" + depends on ARCH_ZX || COMPILE_TEST + depends on HAS_IOMEM + help + Reboot support for ZTE SoCs. + +config REBOOT_MODE + tristate + +config SYSCON_REBOOT_MODE + tristate "Generic SYSCON regmap reboot mode driver" + depends on OF + depends on MFD_SYSCON + select REBOOT_MODE + help + Say y here will enable reboot mode driver. This will + get reboot mode arguments and store it in SYSCON mapped + register, then the bootloader can read it to take different + action according to the mode. + +config POWER_RESET_SC27XX + tristate "Spreadtrum SC27xx PMIC power-off driver" + depends on MFD_SC27XX_PMIC || COMPILE_TEST + help + This driver supports powering off a system through + Spreadtrum SC27xx series PMICs. The SC27xx series + PMICs includes the SC2720, SC2721, SC2723, SC2730 + and SC2731 chips. + +config NVMEM_REBOOT_MODE + tristate "Generic NVMEM reboot mode driver" + depends on OF + select REBOOT_MODE + help + Say y here will enable reboot mode driver. This will + get reboot mode arguments and store it in a NVMEM cell, + then the bootloader can read it and take different + action according to the mode. + +endif + diff --git a/drivers/power/reset/Makefile b/drivers/power/reset/Makefile new file mode 100644 index 000000000..c51eceba9 --- /dev/null +++ b/drivers/power/reset/Makefile @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_POWER_RESET_AS3722) += as3722-poweroff.o +obj-$(CONFIG_POWER_RESET_AT91_POWEROFF) += at91-poweroff.o +obj-$(CONFIG_POWER_RESET_AT91_RESET) += at91-reset.o +obj-$(CONFIG_POWER_RESET_AT91_SAMA5D2_SHDWC) += at91-sama5d2_shdwc.o +obj-$(CONFIG_POWER_RESET_AXXIA) += axxia-reset.o +obj-$(CONFIG_POWER_RESET_BRCMKONA) += brcm-kona-reset.o +obj-$(CONFIG_POWER_RESET_BRCMSTB) += brcmstb-reboot.o +obj-$(CONFIG_POWER_RESET_GEMINI_POWEROFF) += gemini-poweroff.o +obj-$(CONFIG_POWER_RESET_GPIO) += gpio-poweroff.o +obj-$(CONFIG_POWER_RESET_GPIO_RESTART) += gpio-restart.o +obj-$(CONFIG_POWER_RESET_HISI) += hisi-reboot.o +obj-${CONFIG_POWER_RESET_LINKSTATION} += linkstation-poweroff.o +obj-$(CONFIG_POWER_RESET_MSM) += msm-poweroff.o +obj-$(CONFIG_POWER_RESET_MT6323) += mt6323-poweroff.o +obj-$(CONFIG_POWER_RESET_OXNAS) += oxnas-restart.o +obj-$(CONFIG_POWER_RESET_QCOM_PON) += qcom-pon.o +obj-$(CONFIG_POWER_RESET_OCELOT_RESET) += ocelot-reset.o +obj-$(CONFIG_POWER_RESET_PIIX4_POWEROFF) += piix4-poweroff.o +obj-$(CONFIG_POWER_RESET_LTC2952) += ltc2952-poweroff.o +obj-$(CONFIG_POWER_RESET_QNAP) += qnap-poweroff.o +obj-$(CONFIG_POWER_RESET_RESTART) += restart-poweroff.o +obj-$(CONFIG_POWER_RESET_ST) += st-poweroff.o +obj-$(CONFIG_POWER_RESET_VERSATILE) += arm-versatile-reboot.o +obj-$(CONFIG_POWER_RESET_VEXPRESS) += vexpress-poweroff.o +obj-$(CONFIG_POWER_RESET_XGENE) += xgene-reboot.o +obj-$(CONFIG_POWER_RESET_KEYSTONE) += keystone-reset.o +obj-$(CONFIG_POWER_RESET_SYSCON) += syscon-reboot.o +obj-$(CONFIG_POWER_RESET_SYSCON_POWEROFF) += syscon-poweroff.o +obj-$(CONFIG_POWER_RESET_RMOBILE) += rmobile-reset.o +obj-$(CONFIG_POWER_RESET_ZX) += zx-reboot.o +obj-$(CONFIG_REBOOT_MODE) += reboot-mode.o +obj-$(CONFIG_SYSCON_REBOOT_MODE) += syscon-reboot-mode.o +obj-$(CONFIG_POWER_RESET_SC27XX) += sc27xx-poweroff.o +obj-$(CONFIG_NVMEM_REBOOT_MODE) += nvmem-reboot-mode.o diff --git a/drivers/power/reset/arm-versatile-reboot.c b/drivers/power/reset/arm-versatile-reboot.c new file mode 100644 index 000000000..c7624d761 --- /dev/null +++ b/drivers/power/reset/arm-versatile-reboot.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2014 Linaro Ltd. + * + * Author: Linus Walleij <linus.walleij@linaro.org> + */ +#include <linux/init.h> +#include <linux/mfd/syscon.h> +#include <linux/reboot.h> +#include <linux/regmap.h> +#include <linux/of.h> + +#define INTEGRATOR_HDR_CTRL_OFFSET 0x0C +#define INTEGRATOR_HDR_LOCK_OFFSET 0x14 +#define INTEGRATOR_CM_CTRL_RESET (1 << 3) + +#define VERSATILE_SYS_LOCK_OFFSET 0x20 +#define VERSATILE_SYS_RESETCTL_OFFSET 0x40 + +/* Magic unlocking token used on all Versatile boards */ +#define VERSATILE_LOCK_VAL 0xA05F + +/* + * We detect the different syscon types from the compatible strings. + */ +enum versatile_reboot { + INTEGRATOR_REBOOT_CM, + VERSATILE_REBOOT_CM, + REALVIEW_REBOOT_EB, + REALVIEW_REBOOT_PB1176, + REALVIEW_REBOOT_PB11MP, + REALVIEW_REBOOT_PBA8, + REALVIEW_REBOOT_PBX, +}; + +/* Pointer to the system controller */ +static struct regmap *syscon_regmap; +static enum versatile_reboot versatile_reboot_type; + +static const struct of_device_id versatile_reboot_of_match[] = { + { + .compatible = "arm,core-module-integrator", + .data = (void *)INTEGRATOR_REBOOT_CM + }, + { + .compatible = "arm,core-module-versatile", + .data = (void *)VERSATILE_REBOOT_CM, + }, + { + .compatible = "arm,realview-eb-syscon", + .data = (void *)REALVIEW_REBOOT_EB, + }, + { + .compatible = "arm,realview-pb1176-syscon", + .data = (void *)REALVIEW_REBOOT_PB1176, + }, + { + .compatible = "arm,realview-pb11mp-syscon", + .data = (void *)REALVIEW_REBOOT_PB11MP, + }, + { + .compatible = "arm,realview-pba8-syscon", + .data = (void *)REALVIEW_REBOOT_PBA8, + }, + { + .compatible = "arm,realview-pbx-syscon", + .data = (void *)REALVIEW_REBOOT_PBX, + }, + {}, +}; + +static int versatile_reboot(struct notifier_block *this, unsigned long mode, + void *cmd) +{ + /* Unlock the reset register */ + /* Then hit reset on the different machines */ + switch (versatile_reboot_type) { + case INTEGRATOR_REBOOT_CM: + regmap_write(syscon_regmap, INTEGRATOR_HDR_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_update_bits(syscon_regmap, + INTEGRATOR_HDR_CTRL_OFFSET, + INTEGRATOR_CM_CTRL_RESET, + INTEGRATOR_CM_CTRL_RESET); + break; + case VERSATILE_REBOOT_CM: + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_update_bits(syscon_regmap, + VERSATILE_SYS_RESETCTL_OFFSET, + 0x0107, + 0x0105); + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + 0); + break; + case REALVIEW_REBOOT_EB: + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_write(syscon_regmap, + VERSATILE_SYS_RESETCTL_OFFSET, 0x0008); + break; + case REALVIEW_REBOOT_PB1176: + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_write(syscon_regmap, + VERSATILE_SYS_RESETCTL_OFFSET, 0x0100); + break; + case REALVIEW_REBOOT_PB11MP: + case REALVIEW_REBOOT_PBA8: + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_write(syscon_regmap, VERSATILE_SYS_RESETCTL_OFFSET, + 0x0000); + regmap_write(syscon_regmap, VERSATILE_SYS_RESETCTL_OFFSET, + 0x0004); + break; + case REALVIEW_REBOOT_PBX: + regmap_write(syscon_regmap, VERSATILE_SYS_LOCK_OFFSET, + VERSATILE_LOCK_VAL); + regmap_write(syscon_regmap, VERSATILE_SYS_RESETCTL_OFFSET, + 0x00f0); + regmap_write(syscon_regmap, VERSATILE_SYS_RESETCTL_OFFSET, + 0x00f4); + break; + } + dsb(); + + return NOTIFY_DONE; +} + +static struct notifier_block versatile_reboot_nb = { + .notifier_call = versatile_reboot, + .priority = 192, +}; + +static int __init versatile_reboot_probe(void) +{ + const struct of_device_id *reboot_id; + struct device_node *np; + int err; + + np = of_find_matching_node_and_match(NULL, versatile_reboot_of_match, + &reboot_id); + if (!np) + return -ENODEV; + versatile_reboot_type = (enum versatile_reboot)reboot_id->data; + + syscon_regmap = syscon_node_to_regmap(np); + of_node_put(np); + if (IS_ERR(syscon_regmap)) + return PTR_ERR(syscon_regmap); + + err = register_restart_handler(&versatile_reboot_nb); + if (err) + return err; + + pr_info("versatile reboot driver registered\n"); + return 0; +} +device_initcall(versatile_reboot_probe); diff --git a/drivers/power/reset/as3722-poweroff.c b/drivers/power/reset/as3722-poweroff.c new file mode 100644 index 000000000..661e1c67f --- /dev/null +++ b/drivers/power/reset/as3722-poweroff.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Power off driver for ams AS3722 device. + * + * Copyright (c) 2013, NVIDIA CORPORATION. All rights reserved. + * + * Author: Laxman Dewangan <ldewangan@nvidia.com> + */ + +#include <linux/mfd/as3722.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +struct as3722_poweroff { + struct device *dev; + struct as3722 *as3722; +}; + +static struct as3722_poweroff *as3722_pm_poweroff; + +static void as3722_pm_power_off(void) +{ + int ret; + + if (!as3722_pm_poweroff) { + pr_err("AS3722 poweroff is not initialised\n"); + return; + } + + ret = as3722_update_bits(as3722_pm_poweroff->as3722, + AS3722_RESET_CONTROL_REG, AS3722_POWER_OFF, AS3722_POWER_OFF); + if (ret < 0) + dev_err(as3722_pm_poweroff->dev, + "RESET_CONTROL_REG update failed, %d\n", ret); +} + +static int as3722_poweroff_probe(struct platform_device *pdev) +{ + struct as3722_poweroff *as3722_poweroff; + struct device_node *np = pdev->dev.parent->of_node; + + if (!np) + return -EINVAL; + + if (!of_property_read_bool(np, "ams,system-power-controller")) + return 0; + + as3722_poweroff = devm_kzalloc(&pdev->dev, sizeof(*as3722_poweroff), + GFP_KERNEL); + if (!as3722_poweroff) + return -ENOMEM; + + as3722_poweroff->as3722 = dev_get_drvdata(pdev->dev.parent); + as3722_poweroff->dev = &pdev->dev; + as3722_pm_poweroff = as3722_poweroff; + if (!pm_power_off) + pm_power_off = as3722_pm_power_off; + + return 0; +} + +static int as3722_poweroff_remove(struct platform_device *pdev) +{ + if (pm_power_off == as3722_pm_power_off) + pm_power_off = NULL; + as3722_pm_poweroff = NULL; + + return 0; +} + +static struct platform_driver as3722_poweroff_driver = { + .driver = { + .name = "as3722-power-off", + }, + .probe = as3722_poweroff_probe, + .remove = as3722_poweroff_remove, +}; + +module_platform_driver(as3722_poweroff_driver); + +MODULE_DESCRIPTION("Power off driver for ams AS3722 PMIC Device"); +MODULE_ALIAS("platform:as3722-power-off"); +MODULE_AUTHOR("Laxman Dewangan <ldewangan@nvidia.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/at91-poweroff.c b/drivers/power/reset/at91-poweroff.c new file mode 100644 index 000000000..9e74e131c --- /dev/null +++ b/drivers/power/reset/at91-poweroff.c @@ -0,0 +1,239 @@ +/* + * Atmel AT91 SAM9 SoCs reset code + * + * Copyright (C) 2007 Atmel Corporation. + * Copyright (C) 2011 Jean-Christophe PLAGNIOL-VILLARD <plagnioj@jcrosoft.com> + * Copyright (C) 2014 Free Electrons + * + * This file is licensed under the terms of the GNU General Public + * License version 2. This program is licensed "as is" without any + * warranty of any kind, whether express or implied. + */ + +#include <linux/clk.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/printk.h> + +#include <soc/at91/at91sam9_ddrsdr.h> + +#define AT91_SHDW_CR 0x00 /* Shut Down Control Register */ +#define AT91_SHDW_SHDW BIT(0) /* Shut Down command */ +#define AT91_SHDW_KEY (0xa5 << 24) /* KEY Password */ + +#define AT91_SHDW_MR 0x04 /* Shut Down Mode Register */ +#define AT91_SHDW_WKMODE0 GENMASK(2, 0) /* Wake-up 0 Mode Selection */ +#define AT91_SHDW_CPTWK0_MAX 0xf /* Maximum Counter On Wake Up 0 */ +#define AT91_SHDW_CPTWK0 (AT91_SHDW_CPTWK0_MAX << 4) /* Counter On Wake Up 0 */ +#define AT91_SHDW_CPTWK0_(x) ((x) << 4) +#define AT91_SHDW_RTTWKEN BIT(16) /* Real Time Timer Wake-up Enable */ +#define AT91_SHDW_RTCWKEN BIT(17) /* Real Time Clock Wake-up Enable */ + +#define AT91_SHDW_SR 0x08 /* Shut Down Status Register */ +#define AT91_SHDW_WAKEUP0 BIT(0) /* Wake-up 0 Status */ +#define AT91_SHDW_RTTWK BIT(16) /* Real-time Timer Wake-up */ +#define AT91_SHDW_RTCWK BIT(17) /* Real-time Clock Wake-up [SAM9RL] */ + +enum wakeup_type { + AT91_SHDW_WKMODE0_NONE = 0, + AT91_SHDW_WKMODE0_HIGH = 1, + AT91_SHDW_WKMODE0_LOW = 2, + AT91_SHDW_WKMODE0_ANYLEVEL = 3, +}; + +static const char *shdwc_wakeup_modes[] = { + [AT91_SHDW_WKMODE0_NONE] = "none", + [AT91_SHDW_WKMODE0_HIGH] = "high", + [AT91_SHDW_WKMODE0_LOW] = "low", + [AT91_SHDW_WKMODE0_ANYLEVEL] = "any", +}; + +static struct shdwc { + struct clk *sclk; + void __iomem *shdwc_base; + void __iomem *mpddrc_base; +} at91_shdwc; + +static void __init at91_wakeup_status(struct platform_device *pdev) +{ + const char *reason; + u32 reg = readl(at91_shdwc.shdwc_base + AT91_SHDW_SR); + + /* Simple power-on, just bail out */ + if (!reg) + return; + + if (reg & AT91_SHDW_RTTWK) + reason = "RTT"; + else if (reg & AT91_SHDW_RTCWK) + reason = "RTC"; + else + reason = "unknown"; + + dev_info(&pdev->dev, "Wake-Up source: %s\n", reason); +} + +static void at91_poweroff(void) +{ + asm volatile( + /* Align to cache lines */ + ".balign 32\n\t" + + /* Ensure AT91_SHDW_CR is in the TLB by reading it */ + " ldr r6, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" + + /* Power down SDRAM0 */ + " tst %0, #0\n\t" + " beq 1f\n\t" + " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" + /* Shutdown CPU */ + "1: str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" + + " b .\n\t" + : + : "r" (at91_shdwc.mpddrc_base), + "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), + "r" (at91_shdwc.shdwc_base), + "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW) + : "r6"); +} + +static int at91_poweroff_get_wakeup_mode(struct device_node *np) +{ + const char *pm; + unsigned int i; + int err; + + err = of_property_read_string(np, "atmel,wakeup-mode", &pm); + if (err < 0) + return AT91_SHDW_WKMODE0_ANYLEVEL; + + for (i = 0; i < ARRAY_SIZE(shdwc_wakeup_modes); i++) + if (!strcasecmp(pm, shdwc_wakeup_modes[i])) + return i; + + return -ENODEV; +} + +static void at91_poweroff_dt_set_wakeup_mode(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + int wakeup_mode; + u32 mode = 0, tmp; + + wakeup_mode = at91_poweroff_get_wakeup_mode(np); + if (wakeup_mode < 0) { + dev_warn(&pdev->dev, "shdwc unknown wakeup mode\n"); + return; + } + + if (!of_property_read_u32(np, "atmel,wakeup-counter", &tmp)) { + if (tmp > AT91_SHDW_CPTWK0_MAX) { + dev_warn(&pdev->dev, + "shdwc wakeup counter 0x%x > 0x%x reduce it to 0x%x\n", + tmp, AT91_SHDW_CPTWK0_MAX, AT91_SHDW_CPTWK0_MAX); + tmp = AT91_SHDW_CPTWK0_MAX; + } + mode |= AT91_SHDW_CPTWK0_(tmp); + } + + if (of_property_read_bool(np, "atmel,wakeup-rtc-timer")) + mode |= AT91_SHDW_RTCWKEN; + + if (of_property_read_bool(np, "atmel,wakeup-rtt-timer")) + mode |= AT91_SHDW_RTTWKEN; + + writel(wakeup_mode | mode, at91_shdwc.shdwc_base + AT91_SHDW_MR); +} + +static int __init at91_poweroff_probe(struct platform_device *pdev) +{ + struct resource *res; + struct device_node *np; + u32 ddr_type; + int ret; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + at91_shdwc.shdwc_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(at91_shdwc.shdwc_base)) + return PTR_ERR(at91_shdwc.shdwc_base); + + at91_shdwc.sclk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(at91_shdwc.sclk)) + return PTR_ERR(at91_shdwc.sclk); + + ret = clk_prepare_enable(at91_shdwc.sclk); + if (ret) { + dev_err(&pdev->dev, "Could not enable slow clock\n"); + return ret; + } + + at91_wakeup_status(pdev); + + if (pdev->dev.of_node) + at91_poweroff_dt_set_wakeup_mode(pdev); + + np = of_find_compatible_node(NULL, NULL, "atmel,sama5d3-ddramc"); + if (np) { + at91_shdwc.mpddrc_base = of_iomap(np, 0); + of_node_put(np); + + if (!at91_shdwc.mpddrc_base) { + ret = -ENOMEM; + goto clk_disable; + } + + ddr_type = readl(at91_shdwc.mpddrc_base + AT91_DDRSDRC_MDR) & + AT91_DDRSDRC_MD; + if (ddr_type != AT91_DDRSDRC_MD_LPDDR2 && + ddr_type != AT91_DDRSDRC_MD_LPDDR3) { + iounmap(at91_shdwc.mpddrc_base); + at91_shdwc.mpddrc_base = NULL; + } + } + + pm_power_off = at91_poweroff; + + return 0; + +clk_disable: + clk_disable_unprepare(at91_shdwc.sclk); + return ret; +} + +static int __exit at91_poweroff_remove(struct platform_device *pdev) +{ + if (pm_power_off == at91_poweroff) + pm_power_off = NULL; + + if (at91_shdwc.mpddrc_base) + iounmap(at91_shdwc.mpddrc_base); + + clk_disable_unprepare(at91_shdwc.sclk); + + return 0; +} + +static const struct of_device_id at91_poweroff_of_match[] = { + { .compatible = "atmel,at91sam9260-shdwc", }, + { .compatible = "atmel,at91sam9rl-shdwc", }, + { .compatible = "atmel,at91sam9x5-shdwc", }, + { /*sentinel*/ } +}; +MODULE_DEVICE_TABLE(of, at91_poweroff_of_match); + +static struct platform_driver at91_poweroff_driver = { + .remove = __exit_p(at91_poweroff_remove), + .driver = { + .name = "at91-poweroff", + .of_match_table = at91_poweroff_of_match, + }, +}; +module_platform_driver_probe(at91_poweroff_driver, at91_poweroff_probe); + +MODULE_AUTHOR("Atmel Corporation"); +MODULE_DESCRIPTION("Shutdown driver for Atmel SoCs"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/at91-reset.c b/drivers/power/reset/at91-reset.c new file mode 100644 index 000000000..3ff9d93a5 --- /dev/null +++ b/drivers/power/reset/at91-reset.c @@ -0,0 +1,271 @@ +/* + * Atmel AT91 SAM9 & SAMA5 SoCs reset code + * + * Copyright (C) 2007 Atmel Corporation. + * Copyright (C) BitBox Ltd 2010 + * Copyright (C) 2011 Jean-Christophe PLAGNIOL-VILLARD <plagnioj@jcosoft.com> + * Copyright (C) 2014 Free Electrons + * + * This file is licensed under the terms of the GNU General Public + * License version 2. This program is licensed "as is" without any + * warranty of any kind, whether express or implied. + */ + +#include <linux/clk.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> + +#include <soc/at91/at91sam9_ddrsdr.h> +#include <soc/at91/at91sam9_sdramc.h> + +#define AT91_RSTC_CR 0x00 /* Reset Controller Control Register */ +#define AT91_RSTC_PROCRST BIT(0) /* Processor Reset */ +#define AT91_RSTC_PERRST BIT(2) /* Peripheral Reset */ +#define AT91_RSTC_EXTRST BIT(3) /* External Reset */ +#define AT91_RSTC_KEY (0xa5 << 24) /* KEY Password */ + +#define AT91_RSTC_SR 0x04 /* Reset Controller Status Register */ +#define AT91_RSTC_URSTS BIT(0) /* User Reset Status */ +#define AT91_RSTC_RSTTYP GENMASK(10, 8) /* Reset Type */ +#define AT91_RSTC_NRSTL BIT(16) /* NRST Pin Level */ +#define AT91_RSTC_SRCMP BIT(17) /* Software Reset Command in Progress */ + +#define AT91_RSTC_MR 0x08 /* Reset Controller Mode Register */ +#define AT91_RSTC_URSTEN BIT(0) /* User Reset Enable */ +#define AT91_RSTC_URSTASYNC BIT(2) /* User Reset Asynchronous Control */ +#define AT91_RSTC_URSTIEN BIT(4) /* User Reset Interrupt Enable */ +#define AT91_RSTC_ERSTL GENMASK(11, 8) /* External Reset Length */ + +enum reset_type { + RESET_TYPE_GENERAL = 0, + RESET_TYPE_WAKEUP = 1, + RESET_TYPE_WATCHDOG = 2, + RESET_TYPE_SOFTWARE = 3, + RESET_TYPE_USER = 4, + RESET_TYPE_CPU_FAIL = 6, + RESET_TYPE_XTAL_FAIL = 7, + RESET_TYPE_ULP2 = 8, +}; + +struct at91_reset { + void __iomem *rstc_base; + void __iomem *ramc_base[2]; + struct clk *sclk; + struct notifier_block nb; + u32 args; + u32 ramc_lpr; +}; + +/* +* unless the SDRAM is cleanly shutdown before we hit the +* reset register it can be left driving the data bus and +* killing the chance of a subsequent boot from NAND +*/ +static int at91_reset(struct notifier_block *this, unsigned long mode, + void *cmd) +{ + struct at91_reset *reset = container_of(this, struct at91_reset, nb); + + asm volatile( + /* Align to cache lines */ + ".balign 32\n\t" + + /* Disable SDRAM0 accesses */ + " tst %0, #0\n\t" + " beq 1f\n\t" + " str %3, [%0, #" __stringify(AT91_DDRSDRC_RTR) "]\n\t" + /* Power down SDRAM0 */ + " str %4, [%0, %6]\n\t" + /* Disable SDRAM1 accesses */ + "1: tst %1, #0\n\t" + " beq 2f\n\t" + " strne %3, [%1, #" __stringify(AT91_DDRSDRC_RTR) "]\n\t" + /* Power down SDRAM1 */ + " strne %4, [%1, %6]\n\t" + /* Reset CPU */ + "2: str %5, [%2, #" __stringify(AT91_RSTC_CR) "]\n\t" + + " b .\n\t" + : + : "r" (reset->ramc_base[0]), + "r" (reset->ramc_base[1]), + "r" (reset->rstc_base), + "r" (1), + "r" cpu_to_le32(AT91_DDRSDRC_LPCB_POWER_DOWN), + "r" (reset->args), + "r" (reset->ramc_lpr) + : "r4"); + + return NOTIFY_DONE; +} + +static void __init at91_reset_status(struct platform_device *pdev, + void __iomem *base) +{ + const char *reason; + u32 reg = readl(base + AT91_RSTC_SR); + + switch ((reg & AT91_RSTC_RSTTYP) >> 8) { + case RESET_TYPE_GENERAL: + reason = "general reset"; + break; + case RESET_TYPE_WAKEUP: + reason = "wakeup"; + break; + case RESET_TYPE_WATCHDOG: + reason = "watchdog reset"; + break; + case RESET_TYPE_SOFTWARE: + reason = "software reset"; + break; + case RESET_TYPE_USER: + reason = "user reset"; + break; + case RESET_TYPE_CPU_FAIL: + reason = "CPU clock failure detection"; + break; + case RESET_TYPE_XTAL_FAIL: + reason = "32.768 kHz crystal failure detection"; + break; + case RESET_TYPE_ULP2: + reason = "ULP2 reset"; + break; + default: + reason = "unknown reset"; + break; + } + + dev_info(&pdev->dev, "Starting after %s\n", reason); +} + +static const struct of_device_id at91_ramc_of_match[] = { + { + .compatible = "atmel,at91sam9260-sdramc", + .data = (void *)AT91_SDRAMC_LPR, + }, + { + .compatible = "atmel,at91sam9g45-ddramc", + .data = (void *)AT91_DDRSDRC_LPR, + }, + { /* sentinel */ } +}; + +static const struct of_device_id at91_reset_of_match[] = { + { + .compatible = "atmel,at91sam9260-rstc", + .data = (void *)(AT91_RSTC_KEY | AT91_RSTC_PERRST | + AT91_RSTC_PROCRST), + }, + { + .compatible = "atmel,at91sam9g45-rstc", + .data = (void *)(AT91_RSTC_KEY | AT91_RSTC_PERRST | + AT91_RSTC_PROCRST) + }, + { + .compatible = "atmel,sama5d3-rstc", + .data = (void *)(AT91_RSTC_KEY | AT91_RSTC_PERRST | + AT91_RSTC_PROCRST) + }, + { + .compatible = "atmel,samx7-rstc", + .data = (void *)(AT91_RSTC_KEY | AT91_RSTC_PROCRST) + }, + { + .compatible = "microchip,sam9x60-rstc", + .data = (void *)(AT91_RSTC_KEY | AT91_RSTC_PROCRST) + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, at91_reset_of_match); + +static int __init at91_reset_probe(struct platform_device *pdev) +{ + const struct of_device_id *match; + struct at91_reset *reset; + struct device_node *np; + int ret, idx = 0; + + reset = devm_kzalloc(&pdev->dev, sizeof(*reset), GFP_KERNEL); + if (!reset) + return -ENOMEM; + + reset->rstc_base = of_iomap(pdev->dev.of_node, 0); + if (!reset->rstc_base) { + dev_err(&pdev->dev, "Could not map reset controller address\n"); + return -ENODEV; + } + + if (!of_device_is_compatible(pdev->dev.of_node, "atmel,sama5d3-rstc")) { + /* we need to shutdown the ddr controller, so get ramc base */ + for_each_matching_node_and_match(np, at91_ramc_of_match, &match) { + reset->ramc_lpr = (u32)match->data; + reset->ramc_base[idx] = of_iomap(np, 0); + if (!reset->ramc_base[idx]) { + dev_err(&pdev->dev, "Could not map ram controller address\n"); + of_node_put(np); + return -ENODEV; + } + idx++; + } + } + + match = of_match_node(at91_reset_of_match, pdev->dev.of_node); + reset->nb.notifier_call = at91_reset; + reset->nb.priority = 192; + reset->args = (u32)match->data; + + reset->sclk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(reset->sclk)) + return PTR_ERR(reset->sclk); + + ret = clk_prepare_enable(reset->sclk); + if (ret) { + dev_err(&pdev->dev, "Could not enable slow clock\n"); + return ret; + } + + platform_set_drvdata(pdev, reset); + + if (of_device_is_compatible(pdev->dev.of_node, "microchip,sam9x60-rstc")) { + u32 val = readl(reset->rstc_base + AT91_RSTC_MR); + + writel(AT91_RSTC_KEY | AT91_RSTC_URSTASYNC | val, + reset->rstc_base + AT91_RSTC_MR); + } + + ret = register_restart_handler(&reset->nb); + if (ret) { + clk_disable_unprepare(reset->sclk); + return ret; + } + + at91_reset_status(pdev, reset->rstc_base); + + return 0; +} + +static int __exit at91_reset_remove(struct platform_device *pdev) +{ + struct at91_reset *reset = platform_get_drvdata(pdev); + + unregister_restart_handler(&reset->nb); + clk_disable_unprepare(reset->sclk); + + return 0; +} + +static struct platform_driver at91_reset_driver = { + .remove = __exit_p(at91_reset_remove), + .driver = { + .name = "at91-reset", + .of_match_table = at91_reset_of_match, + }, +}; +module_platform_driver_probe(at91_reset_driver, at91_reset_probe); + +MODULE_AUTHOR("Atmel Corporation"); +MODULE_DESCRIPTION("Reset driver for Atmel SoCs"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/at91-sama5d2_shdwc.c b/drivers/power/reset/at91-sama5d2_shdwc.c new file mode 100644 index 000000000..d9cf91e5b --- /dev/null +++ b/drivers/power/reset/at91-sama5d2_shdwc.c @@ -0,0 +1,423 @@ +/* + * Atmel SAMA5D2-Compatible Shutdown Controller (SHDWC) driver. + * Found on some SoCs as the sama5d2 (obviously). + * + * Copyright (C) 2015 Atmel Corporation, + * Nicolas Ferre <nicolas.ferre@atmel.com> + * + * Evolved from driver at91-poweroff.c. + * + * This file is licensed under the terms of the GNU General Public + * License version 2. This program is licensed "as is" without any + * warranty of any kind, whether express or implied. + * + * TODO: + * - addition to status of other wake-up inputs [1 - 15] + * - Analog Comparator wake-up alarm + * - Serial RX wake-up alarm + * - low power debouncer + */ + +#include <linux/clk.h> +#include <linux/clk/at91_pmc.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/printk.h> + +#include <soc/at91/at91sam9_ddrsdr.h> + +#define SLOW_CLOCK_FREQ 32768 + +#define AT91_SHDW_CR 0x00 /* Shut Down Control Register */ +#define AT91_SHDW_SHDW BIT(0) /* Shut Down command */ +#define AT91_SHDW_KEY (0xa5UL << 24) /* KEY Password */ + +#define AT91_SHDW_MR 0x04 /* Shut Down Mode Register */ +#define AT91_SHDW_WKUPDBC_SHIFT 24 +#define AT91_SHDW_WKUPDBC_MASK GENMASK(26, 24) +#define AT91_SHDW_WKUPDBC(x) (((x) << AT91_SHDW_WKUPDBC_SHIFT) \ + & AT91_SHDW_WKUPDBC_MASK) + +#define AT91_SHDW_SR 0x08 /* Shut Down Status Register */ +#define AT91_SHDW_WKUPIS_SHIFT 16 +#define AT91_SHDW_WKUPIS_MASK GENMASK(31, 16) +#define AT91_SHDW_WKUPIS(x) ((1 << (x)) << AT91_SHDW_WKUPIS_SHIFT \ + & AT91_SHDW_WKUPIS_MASK) + +#define AT91_SHDW_WUIR 0x0c /* Shutdown Wake-up Inputs Register */ +#define AT91_SHDW_WKUPEN_MASK GENMASK(15, 0) +#define AT91_SHDW_WKUPEN(x) ((1 << (x)) & AT91_SHDW_WKUPEN_MASK) +#define AT91_SHDW_WKUPT_SHIFT 16 +#define AT91_SHDW_WKUPT_MASK GENMASK(31, 16) +#define AT91_SHDW_WKUPT(x) ((1 << (x)) << AT91_SHDW_WKUPT_SHIFT \ + & AT91_SHDW_WKUPT_MASK) + +#define SHDW_WK_PIN(reg, cfg) ((reg) & AT91_SHDW_WKUPIS((cfg)->wkup_pin_input)) +#define SHDW_RTCWK(reg, cfg) (((reg) >> ((cfg)->sr_rtcwk_shift)) & 0x1) +#define SHDW_RTTWK(reg, cfg) (((reg) >> ((cfg)->sr_rttwk_shift)) & 0x1) +#define SHDW_RTCWKEN(cfg) (1 << ((cfg)->mr_rtcwk_shift)) +#define SHDW_RTTWKEN(cfg) (1 << ((cfg)->mr_rttwk_shift)) + +#define DBC_PERIOD_US(x) DIV_ROUND_UP_ULL((1000000 * (x)), \ + SLOW_CLOCK_FREQ) + +#define SHDW_CFG_NOT_USED (32) + +struct shdwc_reg_config { + u8 wkup_pin_input; + u8 mr_rtcwk_shift; + u8 mr_rttwk_shift; + u8 sr_rtcwk_shift; + u8 sr_rttwk_shift; +}; + +struct pmc_reg_config { + u8 mckr; +}; + +struct reg_config { + struct shdwc_reg_config shdwc; + struct pmc_reg_config pmc; +}; + +struct shdwc { + const struct reg_config *rcfg; + struct clk *sclk; + void __iomem *shdwc_base; + void __iomem *mpddrc_base; + void __iomem *pmc_base; +}; + +/* + * Hold configuration here, cannot be more than one instance of the driver + * since pm_power_off itself is global. + */ +static struct shdwc *at91_shdwc; + +static const unsigned long long sdwc_dbc_period[] = { + 0, 3, 32, 512, 4096, 32768, +}; + +static void __init at91_wakeup_status(struct platform_device *pdev) +{ + struct shdwc *shdw = platform_get_drvdata(pdev); + const struct reg_config *rcfg = shdw->rcfg; + u32 reg; + char *reason = "unknown"; + + reg = readl(shdw->shdwc_base + AT91_SHDW_SR); + + dev_dbg(&pdev->dev, "%s: status = %#x\n", __func__, reg); + + /* Simple power-on, just bail out */ + if (!reg) + return; + + if (SHDW_WK_PIN(reg, &rcfg->shdwc)) + reason = "WKUP pin"; + else if (SHDW_RTCWK(reg, &rcfg->shdwc)) + reason = "RTC"; + else if (SHDW_RTTWK(reg, &rcfg->shdwc)) + reason = "RTT"; + + pr_info("AT91: Wake-Up source: %s\n", reason); +} + +static void at91_poweroff(void) +{ + asm volatile( + /* Align to cache lines */ + ".balign 32\n\t" + + /* Ensure AT91_SHDW_CR is in the TLB by reading it */ + " ldr r6, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" + + /* Power down SDRAM0 */ + " tst %0, #0\n\t" + " beq 1f\n\t" + " str %1, [%0, #" __stringify(AT91_DDRSDRC_LPR) "]\n\t" + + /* Switch the master clock source to slow clock. */ + "1: ldr r6, [%4, %5]\n\t" + " bic r6, r6, #" __stringify(AT91_PMC_CSS) "\n\t" + " str r6, [%4, %5]\n\t" + /* Wait for clock switch. */ + "2: ldr r6, [%4, #" __stringify(AT91_PMC_SR) "]\n\t" + " tst r6, #" __stringify(AT91_PMC_MCKRDY) "\n\t" + " beq 2b\n\t" + + /* Shutdown CPU */ + " str %3, [%2, #" __stringify(AT91_SHDW_CR) "]\n\t" + + " b .\n\t" + : + : "r" (at91_shdwc->mpddrc_base), + "r" cpu_to_le32(AT91_DDRSDRC_LPDDR2_PWOFF), + "r" (at91_shdwc->shdwc_base), + "r" cpu_to_le32(AT91_SHDW_KEY | AT91_SHDW_SHDW), + "r" (at91_shdwc->pmc_base), + "r" (at91_shdwc->rcfg->pmc.mckr) + : "r6"); +} + +static u32 at91_shdwc_debouncer_value(struct platform_device *pdev, + u32 in_period_us) +{ + int i; + int max_idx = ARRAY_SIZE(sdwc_dbc_period) - 1; + unsigned long long period_us; + unsigned long long max_period_us = DBC_PERIOD_US(sdwc_dbc_period[max_idx]); + + if (in_period_us > max_period_us) { + dev_warn(&pdev->dev, + "debouncer period %u too big, reduced to %llu us\n", + in_period_us, max_period_us); + return max_idx; + } + + for (i = max_idx - 1; i > 0; i--) { + period_us = DBC_PERIOD_US(sdwc_dbc_period[i]); + dev_dbg(&pdev->dev, "%s: ref[%d] = %llu\n", + __func__, i, period_us); + if (in_period_us > period_us) + break; + } + + return i + 1; +} + +static u32 at91_shdwc_get_wakeup_input(struct platform_device *pdev, + struct device_node *np) +{ + struct device_node *cnp; + u32 wk_input_mask; + u32 wuir = 0; + u32 wk_input; + + for_each_child_of_node(np, cnp) { + if (of_property_read_u32(cnp, "reg", &wk_input)) { + dev_warn(&pdev->dev, "reg property is missing for %pOF\n", + cnp); + continue; + } + + wk_input_mask = 1 << wk_input; + if (!(wk_input_mask & AT91_SHDW_WKUPEN_MASK)) { + dev_warn(&pdev->dev, + "wake-up input %d out of bounds ignore\n", + wk_input); + continue; + } + wuir |= wk_input_mask; + + if (of_property_read_bool(cnp, "atmel,wakeup-active-high")) + wuir |= AT91_SHDW_WKUPT(wk_input); + + dev_dbg(&pdev->dev, "%s: (child %d) wuir = %#x\n", + __func__, wk_input, wuir); + } + + return wuir; +} + +static void at91_shdwc_dt_configure(struct platform_device *pdev) +{ + struct shdwc *shdw = platform_get_drvdata(pdev); + const struct reg_config *rcfg = shdw->rcfg; + struct device_node *np = pdev->dev.of_node; + u32 mode = 0, tmp, input; + + if (!np) { + dev_err(&pdev->dev, "device node not found\n"); + return; + } + + if (!of_property_read_u32(np, "debounce-delay-us", &tmp)) + mode |= AT91_SHDW_WKUPDBC(at91_shdwc_debouncer_value(pdev, tmp)); + + if (of_property_read_bool(np, "atmel,wakeup-rtc-timer")) + mode |= SHDW_RTCWKEN(&rcfg->shdwc); + + if (of_property_read_bool(np, "atmel,wakeup-rtt-timer")) + mode |= SHDW_RTTWKEN(&rcfg->shdwc); + + dev_dbg(&pdev->dev, "%s: mode = %#x\n", __func__, mode); + writel(mode, shdw->shdwc_base + AT91_SHDW_MR); + + input = at91_shdwc_get_wakeup_input(pdev, np); + writel(input, shdw->shdwc_base + AT91_SHDW_WUIR); +} + +static const struct reg_config sama5d2_reg_config = { + .shdwc = { + .wkup_pin_input = 0, + .mr_rtcwk_shift = 17, + .mr_rttwk_shift = SHDW_CFG_NOT_USED, + .sr_rtcwk_shift = 5, + .sr_rttwk_shift = SHDW_CFG_NOT_USED, + }, + .pmc = { + .mckr = 0x30, + }, +}; + +static const struct reg_config sam9x60_reg_config = { + .shdwc = { + .wkup_pin_input = 0, + .mr_rtcwk_shift = 17, + .mr_rttwk_shift = 16, + .sr_rtcwk_shift = 5, + .sr_rttwk_shift = 4, + }, + .pmc = { + .mckr = 0x28, + }, +}; + +static const struct of_device_id at91_shdwc_of_match[] = { + { + .compatible = "atmel,sama5d2-shdwc", + .data = &sama5d2_reg_config, + }, + { + .compatible = "microchip,sam9x60-shdwc", + .data = &sam9x60_reg_config, + }, { + /*sentinel*/ + } +}; +MODULE_DEVICE_TABLE(of, at91_shdwc_of_match); + +static const struct of_device_id at91_pmc_ids[] = { + { .compatible = "atmel,sama5d2-pmc" }, + { .compatible = "microchip,sam9x60-pmc" }, + { /* Sentinel. */ } +}; + +static int __init at91_shdwc_probe(struct platform_device *pdev) +{ + struct resource *res; + const struct of_device_id *match; + struct device_node *np; + u32 ddr_type; + int ret; + + if (!pdev->dev.of_node) + return -ENODEV; + + if (at91_shdwc) + return -EBUSY; + + at91_shdwc = devm_kzalloc(&pdev->dev, sizeof(*at91_shdwc), GFP_KERNEL); + if (!at91_shdwc) + return -ENOMEM; + + platform_set_drvdata(pdev, at91_shdwc); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + at91_shdwc->shdwc_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(at91_shdwc->shdwc_base)) { + dev_err(&pdev->dev, "Could not map reset controller address\n"); + return PTR_ERR(at91_shdwc->shdwc_base); + } + + match = of_match_node(at91_shdwc_of_match, pdev->dev.of_node); + at91_shdwc->rcfg = match->data; + + at91_shdwc->sclk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(at91_shdwc->sclk)) + return PTR_ERR(at91_shdwc->sclk); + + ret = clk_prepare_enable(at91_shdwc->sclk); + if (ret) { + dev_err(&pdev->dev, "Could not enable slow clock\n"); + return ret; + } + + at91_wakeup_status(pdev); + + at91_shdwc_dt_configure(pdev); + + np = of_find_matching_node(NULL, at91_pmc_ids); + if (!np) { + ret = -ENODEV; + goto clk_disable; + } + + at91_shdwc->pmc_base = of_iomap(np, 0); + of_node_put(np); + + if (!at91_shdwc->pmc_base) { + ret = -ENOMEM; + goto clk_disable; + } + + np = of_find_compatible_node(NULL, NULL, "atmel,sama5d3-ddramc"); + if (!np) { + ret = -ENODEV; + goto unmap; + } + + at91_shdwc->mpddrc_base = of_iomap(np, 0); + of_node_put(np); + + if (!at91_shdwc->mpddrc_base) { + ret = -ENOMEM; + goto unmap; + } + + pm_power_off = at91_poweroff; + + ddr_type = readl(at91_shdwc->mpddrc_base + AT91_DDRSDRC_MDR) & + AT91_DDRSDRC_MD; + if (ddr_type != AT91_DDRSDRC_MD_LPDDR2 && + ddr_type != AT91_DDRSDRC_MD_LPDDR3) { + iounmap(at91_shdwc->mpddrc_base); + at91_shdwc->mpddrc_base = NULL; + } + + return 0; + +unmap: + iounmap(at91_shdwc->pmc_base); +clk_disable: + clk_disable_unprepare(at91_shdwc->sclk); + + return ret; +} + +static int __exit at91_shdwc_remove(struct platform_device *pdev) +{ + struct shdwc *shdw = platform_get_drvdata(pdev); + + if (pm_power_off == at91_poweroff) + pm_power_off = NULL; + + /* Reset values to disable wake-up features */ + writel(0, shdw->shdwc_base + AT91_SHDW_MR); + writel(0, shdw->shdwc_base + AT91_SHDW_WUIR); + + if (shdw->mpddrc_base) + iounmap(shdw->mpddrc_base); + iounmap(shdw->pmc_base); + + clk_disable_unprepare(shdw->sclk); + + return 0; +} + +static struct platform_driver at91_shdwc_driver = { + .remove = __exit_p(at91_shdwc_remove), + .driver = { + .name = "at91-shdwc", + .of_match_table = at91_shdwc_of_match, + }, +}; +module_platform_driver_probe(at91_shdwc_driver, at91_shdwc_probe); + +MODULE_AUTHOR("Nicolas Ferre <nicolas.ferre@atmel.com>"); +MODULE_DESCRIPTION("Atmel shutdown controller driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/axxia-reset.c b/drivers/power/reset/axxia-reset.c new file mode 100644 index 000000000..f7b40be5d --- /dev/null +++ b/drivers/power/reset/axxia-reset.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Reset driver for Axxia devices + * + * Copyright (C) 2014 LSI + */ +#include <linux/init.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/mfd/syscon.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +#define SC_CRIT_WRITE_KEY 0x1000 +#define SC_LATCH_ON_RESET 0x1004 +#define SC_RESET_CONTROL 0x1008 +#define RSTCTL_RST_ZERO (1<<3) +#define RSTCTL_RST_FAB (1<<2) +#define RSTCTL_RST_CHIP (1<<1) +#define RSTCTL_RST_SYS (1<<0) +#define SC_EFUSE_INT_STATUS 0x180c +#define EFUSE_READ_DONE (1<<31) + +static struct regmap *syscon; + +static int axxia_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + /* Access Key (0xab) */ + regmap_write(syscon, SC_CRIT_WRITE_KEY, 0xab); + /* Select internal boot from 0xffff0000 */ + regmap_write(syscon, SC_LATCH_ON_RESET, 0x00000040); + /* Assert ResetReadDone (to avoid hanging in boot ROM) */ + regmap_write(syscon, SC_EFUSE_INT_STATUS, EFUSE_READ_DONE); + /* Assert chip reset */ + regmap_update_bits(syscon, SC_RESET_CONTROL, + RSTCTL_RST_CHIP, RSTCTL_RST_CHIP); + + return NOTIFY_DONE; +} + +static struct notifier_block axxia_restart_nb = { + .notifier_call = axxia_restart_handler, + .priority = 128, +}; + +static int axxia_reset_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + int err; + + syscon = syscon_regmap_lookup_by_phandle(dev->of_node, "syscon"); + if (IS_ERR(syscon)) { + pr_err("%pOFn: syscon lookup failed\n", dev->of_node); + return PTR_ERR(syscon); + } + + err = register_restart_handler(&axxia_restart_nb); + if (err) + dev_err(dev, "cannot register restart handler (err=%d)\n", err); + + return err; +} + +static const struct of_device_id of_axxia_reset_match[] = { + { .compatible = "lsi,axm55xx-reset", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_axxia_reset_match); + +static struct platform_driver axxia_reset_driver = { + .probe = axxia_reset_probe, + .driver = { + .name = "axxia-reset", + .of_match_table = of_match_ptr(of_axxia_reset_match), + }, +}; + +static int __init axxia_reset_init(void) +{ + return platform_driver_register(&axxia_reset_driver); +} +device_initcall(axxia_reset_init); diff --git a/drivers/power/reset/brcm-kona-reset.c b/drivers/power/reset/brcm-kona-reset.c new file mode 100644 index 000000000..8eaa959d8 --- /dev/null +++ b/drivers/power/reset/brcm-kona-reset.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Broadcom + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation version 2. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/io.h> +#include <linux/of_address.h> +#include <linux/of_platform.h> +#include <linux/reboot.h> + +#define RSTMGR_REG_WR_ACCESS_OFFSET 0 +#define RSTMGR_REG_CHIP_SOFT_RST_OFFSET 4 + +#define RSTMGR_WR_PASSWORD 0xa5a5 +#define RSTMGR_WR_PASSWORD_SHIFT 8 +#define RSTMGR_WR_ACCESS_ENABLE 1 + +static void __iomem *kona_reset_base; + +static int kona_reset_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + /* + * A soft reset is triggered by writing a 0 to bit 0 of the soft reset + * register. To write to that register we must first write the password + * and the enable bit in the write access enable register. + */ + writel((RSTMGR_WR_PASSWORD << RSTMGR_WR_PASSWORD_SHIFT) | + RSTMGR_WR_ACCESS_ENABLE, + kona_reset_base + RSTMGR_REG_WR_ACCESS_OFFSET); + writel(0, kona_reset_base + RSTMGR_REG_CHIP_SOFT_RST_OFFSET); + + return NOTIFY_DONE; +} + +static struct notifier_block kona_reset_nb = { + .notifier_call = kona_reset_handler, + .priority = 128, +}; + +static int kona_reset_probe(struct platform_device *pdev) +{ + struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + + kona_reset_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(kona_reset_base)) + return PTR_ERR(kona_reset_base); + + return register_restart_handler(&kona_reset_nb); +} + +static const struct of_device_id of_match[] = { + { .compatible = "brcm,bcm21664-resetmgr" }, + {}, +}; + +static struct platform_driver bcm_kona_reset_driver = { + .probe = kona_reset_probe, + .driver = { + .name = "brcm-kona-reset", + .of_match_table = of_match, + }, +}; + +builtin_platform_driver(bcm_kona_reset_driver); diff --git a/drivers/power/reset/brcmstb-reboot.c b/drivers/power/reset/brcmstb-reboot.c new file mode 100644 index 000000000..884b53c48 --- /dev/null +++ b/drivers/power/reset/brcmstb-reboot.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2013 Broadcom Corporation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation version 2. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/bitops.h> +#include <linux/device.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/jiffies.h> +#include <linux/notifier.h> +#include <linux/of_address.h> +#include <linux/of_irq.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/printk.h> +#include <linux/reboot.h> +#include <linux/regmap.h> +#include <linux/smp.h> +#include <linux/mfd/syscon.h> + +#define RESET_SOURCE_ENABLE_REG 1 +#define SW_MASTER_RESET_REG 2 + +static struct regmap *regmap; +static u32 rst_src_en; +static u32 sw_mstr_rst; + +struct reset_reg_mask { + u32 rst_src_en_mask; + u32 sw_mstr_rst_mask; +}; + +static const struct reset_reg_mask *reset_masks; + +static int brcmstb_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + int rc; + u32 tmp; + + rc = regmap_write(regmap, rst_src_en, reset_masks->rst_src_en_mask); + if (rc) { + pr_err("failed to write rst_src_en (%d)\n", rc); + return NOTIFY_DONE; + } + + rc = regmap_read(regmap, rst_src_en, &tmp); + if (rc) { + pr_err("failed to read rst_src_en (%d)\n", rc); + return NOTIFY_DONE; + } + + rc = regmap_write(regmap, sw_mstr_rst, reset_masks->sw_mstr_rst_mask); + if (rc) { + pr_err("failed to write sw_mstr_rst (%d)\n", rc); + return NOTIFY_DONE; + } + + rc = regmap_read(regmap, sw_mstr_rst, &tmp); + if (rc) { + pr_err("failed to read sw_mstr_rst (%d)\n", rc); + return NOTIFY_DONE; + } + + while (1) + ; + + return NOTIFY_DONE; +} + +static struct notifier_block brcmstb_restart_nb = { + .notifier_call = brcmstb_restart_handler, + .priority = 128, +}; + +static const struct reset_reg_mask reset_bits_40nm = { + .rst_src_en_mask = BIT(0), + .sw_mstr_rst_mask = BIT(0), +}; + +static const struct reset_reg_mask reset_bits_65nm = { + .rst_src_en_mask = BIT(3), + .sw_mstr_rst_mask = BIT(31), +}; + +static const struct of_device_id of_match[] = { + { .compatible = "brcm,brcmstb-reboot", .data = &reset_bits_40nm }, + { .compatible = "brcm,bcm7038-reboot", .data = &reset_bits_65nm }, + {}, +}; + +static int brcmstb_reboot_probe(struct platform_device *pdev) +{ + int rc; + struct device_node *np = pdev->dev.of_node; + const struct of_device_id *of_id; + + of_id = of_match_node(of_match, np); + if (!of_id) { + pr_err("failed to look up compatible string\n"); + return -EINVAL; + } + reset_masks = of_id->data; + + regmap = syscon_regmap_lookup_by_phandle(np, "syscon"); + if (IS_ERR(regmap)) { + pr_err("failed to get syscon phandle\n"); + return -EINVAL; + } + + rc = of_property_read_u32_index(np, "syscon", RESET_SOURCE_ENABLE_REG, + &rst_src_en); + if (rc) { + pr_err("can't get rst_src_en offset (%d)\n", rc); + return -EINVAL; + } + + rc = of_property_read_u32_index(np, "syscon", SW_MASTER_RESET_REG, + &sw_mstr_rst); + if (rc) { + pr_err("can't get sw_mstr_rst offset (%d)\n", rc); + return -EINVAL; + } + + rc = register_restart_handler(&brcmstb_restart_nb); + if (rc) + dev_err(&pdev->dev, + "cannot register restart handler (err=%d)\n", rc); + + return rc; +} + +static struct platform_driver brcmstb_reboot_driver = { + .probe = brcmstb_reboot_probe, + .driver = { + .name = "brcmstb-reboot", + .of_match_table = of_match, + }, +}; + +static int __init brcmstb_reboot_init(void) +{ + return platform_driver_probe(&brcmstb_reboot_driver, + brcmstb_reboot_probe); +} +subsys_initcall(brcmstb_reboot_init); diff --git a/drivers/power/reset/gemini-poweroff.c b/drivers/power/reset/gemini-poweroff.c new file mode 100644 index 000000000..b7f7a8225 --- /dev/null +++ b/drivers/power/reset/gemini-poweroff.c @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Gemini power management controller + * Copyright (C) 2017 Linus Walleij <linus.walleij@linaro.org> + * + * Inspired by code from the SL3516 board support by Jason Lee + * Inspired by code from Janos Laube <janos.dev@gmail.com> + */ +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/bitops.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/reboot.h> + +#define GEMINI_PWC_ID 0x00010500 +#define GEMINI_PWC_IDREG 0x00 +#define GEMINI_PWC_CTRLREG 0x04 +#define GEMINI_PWC_STATREG 0x08 + +#define GEMINI_CTRL_SHUTDOWN BIT(0) +#define GEMINI_CTRL_ENABLE BIT(1) +#define GEMINI_CTRL_IRQ_CLR BIT(2) + +#define GEMINI_STAT_CIR BIT(4) +#define GEMINI_STAT_RTC BIT(5) +#define GEMINI_STAT_POWERBUTTON BIT(6) + +struct gemini_powercon { + struct device *dev; + void __iomem *base; +}; + +static irqreturn_t gemini_powerbutton_interrupt(int irq, void *data) +{ + struct gemini_powercon *gpw = data; + u32 val; + + /* ACK the IRQ */ + val = readl(gpw->base + GEMINI_PWC_CTRLREG); + val |= GEMINI_CTRL_IRQ_CLR; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); + + val = readl(gpw->base + GEMINI_PWC_STATREG); + val &= 0x70U; + switch (val) { + case GEMINI_STAT_CIR: + /* + * We do not yet have a driver for the infrared + * controller so it can cause spurious poweroff + * events. Ignore those for now. + */ + dev_info(gpw->dev, "infrared poweroff - ignored\n"); + break; + case GEMINI_STAT_RTC: + dev_info(gpw->dev, "RTC poweroff\n"); + orderly_poweroff(true); + break; + case GEMINI_STAT_POWERBUTTON: + dev_info(gpw->dev, "poweroff button pressed\n"); + orderly_poweroff(true); + break; + default: + dev_info(gpw->dev, "other power management IRQ\n"); + break; + } + + return IRQ_HANDLED; +} + +/* This callback needs this static local as it has void as argument */ +static struct gemini_powercon *gpw_poweroff; + +static void gemini_poweroff(void) +{ + struct gemini_powercon *gpw = gpw_poweroff; + u32 val; + + dev_crit(gpw->dev, "Gemini power off\n"); + val = readl(gpw->base + GEMINI_PWC_CTRLREG); + val |= GEMINI_CTRL_ENABLE | GEMINI_CTRL_IRQ_CLR; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); + + val &= ~GEMINI_CTRL_ENABLE; + val |= GEMINI_CTRL_SHUTDOWN; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); +} + +static int gemini_poweroff_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct resource *res; + struct gemini_powercon *gpw; + u32 val; + int irq; + int ret; + + gpw = devm_kzalloc(dev, sizeof(*gpw), GFP_KERNEL); + if (!gpw) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + gpw->base = devm_ioremap_resource(dev, res); + if (IS_ERR(gpw->base)) + return PTR_ERR(gpw->base); + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + gpw->dev = dev; + + val = readl(gpw->base + GEMINI_PWC_IDREG); + val &= 0xFFFFFF00U; + if (val != GEMINI_PWC_ID) { + dev_err(dev, "wrong power controller ID: %08x\n", + val); + return -ENODEV; + } + + /* + * Enable the power controller. This is crucial on Gemini + * systems: if this is not done, pressing the power button + * will result in unconditional poweroff without any warning. + * This makes the kernel handle the poweroff. + */ + val = readl(gpw->base + GEMINI_PWC_CTRLREG); + val |= GEMINI_CTRL_ENABLE; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); + + /* Clear the IRQ */ + val = readl(gpw->base + GEMINI_PWC_CTRLREG); + val |= GEMINI_CTRL_IRQ_CLR; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); + + /* Wait for this to clear */ + val = readl(gpw->base + GEMINI_PWC_STATREG); + while (val & 0x70U) + val = readl(gpw->base + GEMINI_PWC_STATREG); + + /* Clear the IRQ again */ + val = readl(gpw->base + GEMINI_PWC_CTRLREG); + val |= GEMINI_CTRL_IRQ_CLR; + writel(val, gpw->base + GEMINI_PWC_CTRLREG); + + ret = devm_request_irq(dev, irq, gemini_powerbutton_interrupt, 0, + "poweroff", gpw); + if (ret) + return ret; + + pm_power_off = gemini_poweroff; + gpw_poweroff = gpw; + + dev_info(dev, "Gemini poweroff driver registered\n"); + + return 0; +} + +static const struct of_device_id gemini_poweroff_of_match[] = { + { + .compatible = "cortina,gemini-power-controller", + }, + {} +}; + +static struct platform_driver gemini_poweroff_driver = { + .probe = gemini_poweroff_probe, + .driver = { + .name = "gemini-poweroff", + .of_match_table = gemini_poweroff_of_match, + }, +}; +builtin_platform_driver(gemini_poweroff_driver); diff --git a/drivers/power/reset/gpio-poweroff.c b/drivers/power/reset/gpio-poweroff.c new file mode 100644 index 000000000..1c5af2fef --- /dev/null +++ b/drivers/power/reset/gpio-poweroff.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Toggles a GPIO pin to power down a device + * + * Jamie Lentin <jm@lentin.co.uk> + * Andrew Lunn <andrew@lunn.ch> + * + * Copyright (C) 2012 Jamie Lentin + */ +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/gpio/consumer.h> +#include <linux/of_platform.h> +#include <linux/module.h> + +#define DEFAULT_TIMEOUT_MS 3000 +/* + * Hold configuration here, cannot be more than one instance of the driver + * since pm_power_off itself is global. + */ +static struct gpio_desc *reset_gpio; +static u32 timeout = DEFAULT_TIMEOUT_MS; +static u32 active_delay = 100; +static u32 inactive_delay = 100; + +static void gpio_poweroff_do_poweroff(void) +{ + BUG_ON(!reset_gpio); + + /* drive it active, also inactive->active edge */ + gpiod_direction_output(reset_gpio, 1); + mdelay(active_delay); + + /* drive inactive, also active->inactive edge */ + gpiod_set_value_cansleep(reset_gpio, 0); + mdelay(inactive_delay); + + /* drive it active, also inactive->active edge */ + gpiod_set_value_cansleep(reset_gpio, 1); + + /* give it some time */ + mdelay(timeout); + + WARN_ON(1); +} + +static int gpio_poweroff_probe(struct platform_device *pdev) +{ + bool input = false; + enum gpiod_flags flags; + + /* If a pm_power_off function has already been added, leave it alone */ + if (pm_power_off != NULL) { + dev_err(&pdev->dev, + "%s: pm_power_off function already registered\n", + __func__); + return -EBUSY; + } + + input = device_property_read_bool(&pdev->dev, "input"); + if (input) + flags = GPIOD_IN; + else + flags = GPIOD_OUT_LOW; + + device_property_read_u32(&pdev->dev, "active-delay-ms", &active_delay); + device_property_read_u32(&pdev->dev, "inactive-delay-ms", + &inactive_delay); + device_property_read_u32(&pdev->dev, "timeout-ms", &timeout); + + reset_gpio = devm_gpiod_get(&pdev->dev, NULL, flags); + if (IS_ERR(reset_gpio)) + return PTR_ERR(reset_gpio); + + pm_power_off = &gpio_poweroff_do_poweroff; + return 0; +} + +static int gpio_poweroff_remove(struct platform_device *pdev) +{ + if (pm_power_off == &gpio_poweroff_do_poweroff) + pm_power_off = NULL; + + return 0; +} + +static const struct of_device_id of_gpio_poweroff_match[] = { + { .compatible = "gpio-poweroff", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_gpio_poweroff_match); + +static struct platform_driver gpio_poweroff_driver = { + .probe = gpio_poweroff_probe, + .remove = gpio_poweroff_remove, + .driver = { + .name = "poweroff-gpio", + .of_match_table = of_gpio_poweroff_match, + }, +}; + +module_platform_driver(gpio_poweroff_driver); + +MODULE_AUTHOR("Jamie Lentin <jm@lentin.co.uk>"); +MODULE_DESCRIPTION("GPIO poweroff driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:poweroff-gpio"); diff --git a/drivers/power/reset/gpio-restart.c b/drivers/power/reset/gpio-restart.c new file mode 100644 index 000000000..5466eeea2 --- /dev/null +++ b/drivers/power/reset/gpio-restart.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Toggles a GPIO pin to restart a device + * + * Copyright (C) 2014 Google, Inc. + * + * Based on the gpio-poweroff driver. + */ +#include <linux/reboot.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/gpio/consumer.h> +#include <linux/of_platform.h> +#include <linux/module.h> + +struct gpio_restart { + struct gpio_desc *reset_gpio; + struct notifier_block restart_handler; + u32 active_delay_ms; + u32 inactive_delay_ms; + u32 wait_delay_ms; +}; + +static int gpio_restart_notify(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct gpio_restart *gpio_restart = + container_of(this, struct gpio_restart, restart_handler); + + /* drive it active, also inactive->active edge */ + gpiod_direction_output(gpio_restart->reset_gpio, 1); + mdelay(gpio_restart->active_delay_ms); + + /* drive inactive, also active->inactive edge */ + gpiod_set_value(gpio_restart->reset_gpio, 0); + mdelay(gpio_restart->inactive_delay_ms); + + /* drive it active, also inactive->active edge */ + gpiod_set_value(gpio_restart->reset_gpio, 1); + + /* give it some time */ + mdelay(gpio_restart->wait_delay_ms); + + WARN_ON(1); + + return NOTIFY_DONE; +} + +static int gpio_restart_probe(struct platform_device *pdev) +{ + struct gpio_restart *gpio_restart; + bool open_source = false; + u32 property; + int ret; + + gpio_restart = devm_kzalloc(&pdev->dev, sizeof(*gpio_restart), + GFP_KERNEL); + if (!gpio_restart) + return -ENOMEM; + + open_source = of_property_read_bool(pdev->dev.of_node, "open-source"); + + gpio_restart->reset_gpio = devm_gpiod_get(&pdev->dev, NULL, + open_source ? GPIOD_IN : GPIOD_OUT_LOW); + ret = PTR_ERR_OR_ZERO(gpio_restart->reset_gpio); + if (ret) { + if (ret != -EPROBE_DEFER) + dev_err(&pdev->dev, "Could not get reset GPIO\n"); + return ret; + } + + gpio_restart->restart_handler.notifier_call = gpio_restart_notify; + gpio_restart->restart_handler.priority = 129; + gpio_restart->active_delay_ms = 100; + gpio_restart->inactive_delay_ms = 100; + gpio_restart->wait_delay_ms = 3000; + + ret = of_property_read_u32(pdev->dev.of_node, "priority", &property); + if (!ret) { + if (property > 255) + dev_err(&pdev->dev, "Invalid priority property: %u\n", + property); + else + gpio_restart->restart_handler.priority = property; + } + + of_property_read_u32(pdev->dev.of_node, "active-delay", + &gpio_restart->active_delay_ms); + of_property_read_u32(pdev->dev.of_node, "inactive-delay", + &gpio_restart->inactive_delay_ms); + of_property_read_u32(pdev->dev.of_node, "wait-delay", + &gpio_restart->wait_delay_ms); + + platform_set_drvdata(pdev, gpio_restart); + + ret = register_restart_handler(&gpio_restart->restart_handler); + if (ret) { + dev_err(&pdev->dev, "%s: cannot register restart handler, %d\n", + __func__, ret); + return -ENODEV; + } + + return 0; +} + +static int gpio_restart_remove(struct platform_device *pdev) +{ + struct gpio_restart *gpio_restart = platform_get_drvdata(pdev); + int ret; + + ret = unregister_restart_handler(&gpio_restart->restart_handler); + if (ret) { + dev_err(&pdev->dev, + "%s: cannot unregister restart handler, %d\n", + __func__, ret); + return -ENODEV; + } + + return 0; +} + +static const struct of_device_id of_gpio_restart_match[] = { + { .compatible = "gpio-restart", }, + {}, +}; + +static struct platform_driver gpio_restart_driver = { + .probe = gpio_restart_probe, + .remove = gpio_restart_remove, + .driver = { + .name = "restart-gpio", + .of_match_table = of_gpio_restart_match, + }, +}; + +module_platform_driver(gpio_restart_driver); + +MODULE_AUTHOR("David Riley <davidriley@chromium.org>"); +MODULE_DESCRIPTION("GPIO restart driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/reset/hisi-reboot.c b/drivers/power/reset/hisi-reboot.c new file mode 100644 index 000000000..0ba5fdce1 --- /dev/null +++ b/drivers/power/reset/hisi-reboot.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Hisilicon SoC reset code + * + * Copyright (c) 2014 Hisilicon Ltd. + * Copyright (c) 2014 Linaro Ltd. + * + * Author: Haojian Zhuang <haojian.zhuang@linaro.org> + */ + +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> + +#include <asm/proc-fns.h> + +static void __iomem *base; +static u32 reboot_offset; + +static int hisi_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + writel_relaxed(0xdeadbeef, base + reboot_offset); + + while (1) + cpu_do_idle(); + + return NOTIFY_DONE; +} + +static struct notifier_block hisi_restart_nb = { + .notifier_call = hisi_restart_handler, + .priority = 128, +}; + +static int hisi_reboot_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + int err; + + base = of_iomap(np, 0); + if (!base) { + WARN(1, "failed to map base address"); + return -ENODEV; + } + + if (of_property_read_u32(np, "reboot-offset", &reboot_offset) < 0) { + pr_err("failed to find reboot-offset property\n"); + iounmap(base); + return -EINVAL; + } + + err = register_restart_handler(&hisi_restart_nb); + if (err) { + dev_err(&pdev->dev, "cannot register restart handler (err=%d)\n", + err); + iounmap(base); + } + + return err; +} + +static const struct of_device_id hisi_reboot_of_match[] = { + { .compatible = "hisilicon,sysctrl" }, + {} +}; + +static struct platform_driver hisi_reboot_driver = { + .probe = hisi_reboot_probe, + .driver = { + .name = "hisi-reboot", + .of_match_table = hisi_reboot_of_match, + }, +}; +module_platform_driver(hisi_reboot_driver); diff --git a/drivers/power/reset/keystone-reset.c b/drivers/power/reset/keystone-reset.c new file mode 100644 index 000000000..211eeef0c --- /dev/null +++ b/drivers/power/reset/keystone-reset.c @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TI keystone reboot driver + * + * Copyright (C) 2014 Texas Instruments Incorporated. https://www.ti.com/ + * + * Author: Ivan Khoronzhuk <ivan.khoronzhuk@ti.com> + */ + +#include <linux/io.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/reboot.h> +#include <linux/regmap.h> +#include <linux/mfd/syscon.h> +#include <linux/of_platform.h> + +#define RSTYPE_RG 0x0 +#define RSCTRL_RG 0x4 +#define RSCFG_RG 0x8 +#define RSISO_RG 0xc + +#define RSCTRL_KEY_MASK 0x0000ffff +#define RSCTRL_RESET_MASK BIT(16) +#define RSCTRL_KEY 0x5a69 + +#define RSMUX_OMODE_MASK 0xe +#define RSMUX_OMODE_RESET_ON 0xa +#define RSMUX_OMODE_RESET_OFF 0x0 +#define RSMUX_LOCK_MASK 0x1 +#define RSMUX_LOCK_SET 0x1 + +#define RSCFG_RSTYPE_SOFT 0x300f +#define RSCFG_RSTYPE_HARD 0x0 + +#define WDT_MUX_NUMBER 0x4 + +static int rspll_offset; +static struct regmap *pllctrl_regs; + +/** + * rsctrl_enable_rspll_write - enable access to RSCTRL, RSCFG + * To be able to access to RSCTRL, RSCFG registers + * we have to write a key before + */ +static inline int rsctrl_enable_rspll_write(void) +{ + return regmap_update_bits(pllctrl_regs, rspll_offset + RSCTRL_RG, + RSCTRL_KEY_MASK, RSCTRL_KEY); +} + +static int rsctrl_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + /* enable write access to RSTCTRL */ + rsctrl_enable_rspll_write(); + + /* reset the SOC */ + regmap_update_bits(pllctrl_regs, rspll_offset + RSCTRL_RG, + RSCTRL_RESET_MASK, 0); + + return NOTIFY_DONE; +} + +static struct notifier_block rsctrl_restart_nb = { + .notifier_call = rsctrl_restart_handler, + .priority = 128, +}; + +static const struct of_device_id rsctrl_of_match[] = { + {.compatible = "ti,keystone-reset", }, + {}, +}; + +static int rsctrl_probe(struct platform_device *pdev) +{ + int i; + int ret; + u32 val; + unsigned int rg; + u32 rsmux_offset; + struct regmap *devctrl_regs; + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + + if (!np) + return -ENODEV; + + /* get regmaps */ + pllctrl_regs = syscon_regmap_lookup_by_phandle(np, "ti,syscon-pll"); + if (IS_ERR(pllctrl_regs)) + return PTR_ERR(pllctrl_regs); + + devctrl_regs = syscon_regmap_lookup_by_phandle(np, "ti,syscon-dev"); + if (IS_ERR(devctrl_regs)) + return PTR_ERR(devctrl_regs); + + ret = of_property_read_u32_index(np, "ti,syscon-pll", 1, &rspll_offset); + if (ret) { + dev_err(dev, "couldn't read the reset pll offset!\n"); + return -EINVAL; + } + + ret = of_property_read_u32_index(np, "ti,syscon-dev", 1, &rsmux_offset); + if (ret) { + dev_err(dev, "couldn't read the rsmux offset!\n"); + return -EINVAL; + } + + /* set soft/hard reset */ + val = of_property_read_bool(np, "ti,soft-reset"); + val = val ? RSCFG_RSTYPE_SOFT : RSCFG_RSTYPE_HARD; + + ret = rsctrl_enable_rspll_write(); + if (ret) + return ret; + + ret = regmap_write(pllctrl_regs, rspll_offset + RSCFG_RG, val); + if (ret) + return ret; + + /* disable a reset isolation for all module clocks */ + ret = regmap_write(pllctrl_regs, rspll_offset + RSISO_RG, 0); + if (ret) + return ret; + + /* enable a reset for watchdogs from wdt-list */ + for (i = 0; i < WDT_MUX_NUMBER; i++) { + ret = of_property_read_u32_index(np, "ti,wdt-list", i, &val); + if (ret == -EOVERFLOW && !i) { + dev_err(dev, "ti,wdt-list property has to contain at" + "least one entry\n"); + return -EINVAL; + } else if (ret) { + break; + } + + if (val >= WDT_MUX_NUMBER) { + dev_err(dev, "ti,wdt-list property can contain " + "only numbers < 4\n"); + return -EINVAL; + } + + rg = rsmux_offset + val * 4; + + ret = regmap_update_bits(devctrl_regs, rg, RSMUX_OMODE_MASK, + RSMUX_OMODE_RESET_ON | + RSMUX_LOCK_SET); + if (ret) + return ret; + } + + ret = register_restart_handler(&rsctrl_restart_nb); + if (ret) + dev_err(dev, "cannot register restart handler (err=%d)\n", ret); + + return ret; +} + +static struct platform_driver rsctrl_driver = { + .probe = rsctrl_probe, + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = rsctrl_of_match, + }, +}; +module_platform_driver(rsctrl_driver); + +MODULE_AUTHOR("Ivan Khoronzhuk <ivan.khoronzhuk@ti.com>"); +MODULE_DESCRIPTION("Texas Instruments keystone reset driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:" KBUILD_MODNAME); diff --git a/drivers/power/reset/linkstation-poweroff.c b/drivers/power/reset/linkstation-poweroff.c new file mode 100644 index 000000000..39e89baed --- /dev/null +++ b/drivers/power/reset/linkstation-poweroff.c @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * LinkStation power off restart driver + * Copyright (C) 2020 Daniel González Cabanelas <dgcbueu@gmail.com> + */ + +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/of_mdio.h> +#include <linux/of_platform.h> +#include <linux/reboot.h> +#include <linux/phy.h> + +/* Defines from the eth phy Marvell driver */ +#define MII_MARVELL_COPPER_PAGE 0 +#define MII_MARVELL_LED_PAGE 3 +#define MII_MARVELL_WOL_PAGE 17 +#define MII_MARVELL_PHY_PAGE 22 + +#define MII_PHY_LED_CTRL 16 +#define MII_88E1318S_PHY_LED_TCR 18 +#define MII_88E1318S_PHY_WOL_CTRL 16 +#define MII_M1011_IEVENT 19 + +#define MII_88E1318S_PHY_LED_TCR_INTn_ENABLE BIT(7) +#define MII_88E1318S_PHY_LED_TCR_FORCE_INT BIT(15) +#define MII_88E1318S_PHY_WOL_CTRL_CLEAR_WOL_STATUS BIT(12) +#define LED2_FORCE_ON (0x8 << 8) +#define LEDMASK GENMASK(11,8) + +static struct phy_device *phydev; + +static void mvphy_reg_intn(u16 data) +{ + int rc = 0, saved_page; + + saved_page = phy_select_page(phydev, MII_MARVELL_LED_PAGE); + if (saved_page < 0) + goto err; + + /* Force manual LED2 control to let INTn work */ + __phy_modify(phydev, MII_PHY_LED_CTRL, LEDMASK, LED2_FORCE_ON); + + /* Set the LED[2]/INTn pin to the required state */ + __phy_modify(phydev, MII_88E1318S_PHY_LED_TCR, + MII_88E1318S_PHY_LED_TCR_FORCE_INT, + MII_88E1318S_PHY_LED_TCR_INTn_ENABLE | data); + + if (!data) { + /* Clear interrupts to ensure INTn won't be holded in high state */ + __phy_write(phydev, MII_MARVELL_PHY_PAGE, MII_MARVELL_COPPER_PAGE); + __phy_read(phydev, MII_M1011_IEVENT); + + /* If WOL was enabled and a magic packet was received before powering + * off, we won't be able to wake up by sending another magic packet. + * Clear WOL status. + */ + __phy_write(phydev, MII_MARVELL_PHY_PAGE, MII_MARVELL_WOL_PAGE); + __phy_set_bits(phydev, MII_88E1318S_PHY_WOL_CTRL, + MII_88E1318S_PHY_WOL_CTRL_CLEAR_WOL_STATUS); + } +err: + rc = phy_restore_page(phydev, saved_page, rc); + if (rc < 0) + dev_err(&phydev->mdio.dev, "Write register failed, %d\n", rc); +} + +static int linkstation_reboot_notifier(struct notifier_block *nb, + unsigned long action, void *unused) +{ + if (action == SYS_RESTART) + mvphy_reg_intn(MII_88E1318S_PHY_LED_TCR_FORCE_INT); + + return NOTIFY_DONE; +} + +static struct notifier_block linkstation_reboot_nb = { + .notifier_call = linkstation_reboot_notifier, +}; + +static void linkstation_poweroff(void) +{ + unregister_reboot_notifier(&linkstation_reboot_nb); + mvphy_reg_intn(0); + + kernel_restart("Power off"); +} + +static const struct of_device_id ls_poweroff_of_match[] = { + { .compatible = "buffalo,ls421d" }, + { .compatible = "buffalo,ls421de" }, + { }, +}; + +static int __init linkstation_poweroff_init(void) +{ + struct mii_bus *bus; + struct device_node *dn; + + dn = of_find_matching_node(NULL, ls_poweroff_of_match); + if (!dn) + return -ENODEV; + of_node_put(dn); + + dn = of_find_node_by_name(NULL, "mdio"); + if (!dn) + return -ENODEV; + + bus = of_mdio_find_bus(dn); + of_node_put(dn); + if (!bus) + return -EPROBE_DEFER; + + phydev = phy_find_first(bus); + if (!phydev) + return -EPROBE_DEFER; + + register_reboot_notifier(&linkstation_reboot_nb); + pm_power_off = linkstation_poweroff; + + return 0; +} + +static void __exit linkstation_poweroff_exit(void) +{ + pm_power_off = NULL; + unregister_reboot_notifier(&linkstation_reboot_nb); +} + +module_init(linkstation_poweroff_init); +module_exit(linkstation_poweroff_exit); + +MODULE_AUTHOR("Daniel González Cabanelas <dgcbueu@gmail.com>"); +MODULE_DESCRIPTION("LinkStation power off driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/ltc2952-poweroff.c b/drivers/power/reset/ltc2952-poweroff.c new file mode 100644 index 000000000..d3844ae9e --- /dev/null +++ b/drivers/power/reset/ltc2952-poweroff.c @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LTC2952 (PowerPath) driver + * + * Copyright (C) 2014, Xsens Technologies BV <info@xsens.com> + * Maintainer: René Moll <linux@r-moll.nl> + * + * ---------------------------------------- + * - Description + * ---------------------------------------- + * + * This driver is to be used with an external PowerPath Controller (LTC2952). + * Its function is to determine when a external shut down is triggered + * and react by properly shutting down the system. + * + * This driver expects a device tree with a ltc2952 entry for pin mapping. + * + * ---------------------------------------- + * - GPIO + * ---------------------------------------- + * + * The following GPIOs are used: + * - trigger (input) + * A level change indicates the shut-down trigger. If it's state reverts + * within the time-out defined by trigger_delay, the shut down is not + * executed. If no pin is assigned to this input, the driver will start the + * watchdog toggle immediately. The chip will only power off the system if + * it is requested to do so through the kill line. + * + * - watchdog (output) + * Once a shut down is triggered, the driver will toggle this signal, + * with an internal (wde_interval) to stall the hardware shut down. + * + * - kill (output) + * The last action during shut down is triggering this signalling, such + * that the PowerPath Control will power down the hardware. + * + * ---------------------------------------- + * - Interrupts + * ---------------------------------------- + * + * The driver requires a non-shared, edge-triggered interrupt on the trigger + * GPIO. + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/ktime.h> +#include <linux/slab.h> +#include <linux/kmod.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/gpio/consumer.h> +#include <linux/reboot.h> + +struct ltc2952_poweroff { + struct hrtimer timer_trigger; + struct hrtimer timer_wde; + + ktime_t trigger_delay; + ktime_t wde_interval; + + struct device *dev; + + struct gpio_desc *gpio_trigger; + struct gpio_desc *gpio_watchdog; + struct gpio_desc *gpio_kill; + + bool kernel_panic; + struct notifier_block panic_notifier; +}; + +#define to_ltc2952(p, m) container_of(p, struct ltc2952_poweroff, m) + +/* + * This global variable is only needed for pm_power_off. We should + * remove it entirely once we don't need the global state anymore. + */ +static struct ltc2952_poweroff *ltc2952_data; + +/** + * ltc2952_poweroff_timer_wde - Timer callback + * Toggles the watchdog reset signal each wde_interval + * + * @timer: corresponding timer + * + * Returns HRTIMER_RESTART for an infinite loop which will only stop when the + * machine actually shuts down + */ +static enum hrtimer_restart ltc2952_poweroff_timer_wde(struct hrtimer *timer) +{ + ktime_t now; + int state; + struct ltc2952_poweroff *data = to_ltc2952(timer, timer_wde); + + if (data->kernel_panic) + return HRTIMER_NORESTART; + + state = gpiod_get_value(data->gpio_watchdog); + gpiod_set_value(data->gpio_watchdog, !state); + + now = hrtimer_cb_get_time(timer); + hrtimer_forward(timer, now, data->wde_interval); + + return HRTIMER_RESTART; +} + +static void ltc2952_poweroff_start_wde(struct ltc2952_poweroff *data) +{ + hrtimer_start(&data->timer_wde, data->wde_interval, HRTIMER_MODE_REL); +} + +static enum hrtimer_restart +ltc2952_poweroff_timer_trigger(struct hrtimer *timer) +{ + struct ltc2952_poweroff *data = to_ltc2952(timer, timer_trigger); + + ltc2952_poweroff_start_wde(data); + dev_info(data->dev, "executing shutdown\n"); + orderly_poweroff(true); + + return HRTIMER_NORESTART; +} + +/** + * ltc2952_poweroff_handler - Interrupt handler + * Triggered each time the trigger signal changes state and (de)activates a + * time-out (timer_trigger). Once the time-out is actually reached the shut + * down is executed. + * + * @irq: IRQ number + * @dev_id: pointer to the main data structure + */ +static irqreturn_t ltc2952_poweroff_handler(int irq, void *dev_id) +{ + struct ltc2952_poweroff *data = dev_id; + + if (data->kernel_panic || hrtimer_active(&data->timer_wde)) { + /* shutdown is already triggered, nothing to do any more */ + return IRQ_HANDLED; + } + + if (gpiod_get_value(data->gpio_trigger)) { + hrtimer_start(&data->timer_trigger, data->trigger_delay, + HRTIMER_MODE_REL); + } else { + hrtimer_cancel(&data->timer_trigger); + } + return IRQ_HANDLED; +} + +static void ltc2952_poweroff_kill(void) +{ + gpiod_set_value(ltc2952_data->gpio_kill, 1); +} + +static void ltc2952_poweroff_default(struct ltc2952_poweroff *data) +{ + data->wde_interval = 300L * NSEC_PER_MSEC; + data->trigger_delay = ktime_set(2, 500L * NSEC_PER_MSEC); + + hrtimer_init(&data->timer_trigger, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + data->timer_trigger.function = ltc2952_poweroff_timer_trigger; + + hrtimer_init(&data->timer_wde, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + data->timer_wde.function = ltc2952_poweroff_timer_wde; +} + +static int ltc2952_poweroff_init(struct platform_device *pdev) +{ + int ret; + struct ltc2952_poweroff *data = platform_get_drvdata(pdev); + + ltc2952_poweroff_default(data); + + data->gpio_watchdog = devm_gpiod_get(&pdev->dev, "watchdog", + GPIOD_OUT_LOW); + if (IS_ERR(data->gpio_watchdog)) { + ret = PTR_ERR(data->gpio_watchdog); + dev_err(&pdev->dev, "unable to claim gpio \"watchdog\"\n"); + return ret; + } + + data->gpio_kill = devm_gpiod_get(&pdev->dev, "kill", GPIOD_OUT_LOW); + if (IS_ERR(data->gpio_kill)) { + ret = PTR_ERR(data->gpio_kill); + dev_err(&pdev->dev, "unable to claim gpio \"kill\"\n"); + return ret; + } + + data->gpio_trigger = devm_gpiod_get_optional(&pdev->dev, "trigger", + GPIOD_IN); + if (IS_ERR(data->gpio_trigger)) { + /* + * It's not a problem if the trigger gpio isn't available, but + * it is worth a warning if its use was defined in the device + * tree. + */ + dev_err(&pdev->dev, "unable to claim gpio \"trigger\"\n"); + data->gpio_trigger = NULL; + } + + if (devm_request_irq(&pdev->dev, gpiod_to_irq(data->gpio_trigger), + ltc2952_poweroff_handler, + (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING), + "ltc2952-poweroff", + data)) { + /* + * Some things may have happened: + * - No trigger input was defined + * - Claiming the GPIO failed + * - We could not map to an IRQ + * - We couldn't register an interrupt handler + * + * None of these really are problems, but all of them + * disqualify the push button from controlling the power. + * + * It is therefore important to note that if the ltc2952 + * detects a button press for long enough, it will still start + * its own powerdown window and cut the power on us if we don't + * start the watchdog trigger. + */ + if (data->gpio_trigger) { + dev_warn(&pdev->dev, + "unable to configure the trigger interrupt\n"); + devm_gpiod_put(&pdev->dev, data->gpio_trigger); + data->gpio_trigger = NULL; + } + dev_info(&pdev->dev, + "power down trigger input will not be used\n"); + ltc2952_poweroff_start_wde(data); + } + + return 0; +} + +static int ltc2952_poweroff_notify_panic(struct notifier_block *nb, + unsigned long code, void *unused) +{ + struct ltc2952_poweroff *data = to_ltc2952(nb, panic_notifier); + + data->kernel_panic = true; + return NOTIFY_DONE; +} + +static int ltc2952_poweroff_probe(struct platform_device *pdev) +{ + int ret; + struct ltc2952_poweroff *data; + + if (pm_power_off) { + dev_err(&pdev->dev, "pm_power_off already registered"); + return -EBUSY; + } + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->dev = &pdev->dev; + platform_set_drvdata(pdev, data); + + ret = ltc2952_poweroff_init(pdev); + if (ret) + return ret; + + /* TODO: remove ltc2952_data */ + ltc2952_data = data; + pm_power_off = ltc2952_poweroff_kill; + + data->panic_notifier.notifier_call = ltc2952_poweroff_notify_panic; + atomic_notifier_chain_register(&panic_notifier_list, + &data->panic_notifier); + dev_info(&pdev->dev, "probe successful\n"); + + return 0; +} + +static int ltc2952_poweroff_remove(struct platform_device *pdev) +{ + struct ltc2952_poweroff *data = platform_get_drvdata(pdev); + + pm_power_off = NULL; + hrtimer_cancel(&data->timer_trigger); + hrtimer_cancel(&data->timer_wde); + atomic_notifier_chain_unregister(&panic_notifier_list, + &data->panic_notifier); + return 0; +} + +static const struct of_device_id of_ltc2952_poweroff_match[] = { + { .compatible = "lltc,ltc2952"}, + {}, +}; +MODULE_DEVICE_TABLE(of, of_ltc2952_poweroff_match); + +static struct platform_driver ltc2952_poweroff_driver = { + .probe = ltc2952_poweroff_probe, + .remove = ltc2952_poweroff_remove, + .driver = { + .name = "ltc2952-poweroff", + .of_match_table = of_ltc2952_poweroff_match, + }, +}; + +module_platform_driver(ltc2952_poweroff_driver); + +MODULE_AUTHOR("René Moll <rene.moll@xsens.com>"); +MODULE_DESCRIPTION("LTC PowerPath power-off driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/msm-poweroff.c b/drivers/power/reset/msm-poweroff.c new file mode 100644 index 000000000..0c439f83b --- /dev/null +++ b/drivers/power/reset/msm-poweroff.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2013, The Linux Foundation. All rights reserved. + */ + +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/io.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/module.h> +#include <linux/reboot.h> +#include <linux/pm.h> + +static void __iomem *msm_ps_hold; +static int deassert_pshold(struct notifier_block *nb, unsigned long action, + void *data) +{ + writel(0, msm_ps_hold); + mdelay(10000); + + return NOTIFY_DONE; +} + +static struct notifier_block restart_nb = { + .notifier_call = deassert_pshold, + .priority = 128, +}; + +static void do_msm_poweroff(void) +{ + deassert_pshold(&restart_nb, 0, NULL); +} + +static int msm_restart_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct resource *mem; + + mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); + msm_ps_hold = devm_ioremap_resource(dev, mem); + if (IS_ERR(msm_ps_hold)) + return PTR_ERR(msm_ps_hold); + + register_restart_handler(&restart_nb); + + pm_power_off = do_msm_poweroff; + + return 0; +} + +static const struct of_device_id of_msm_restart_match[] = { + { .compatible = "qcom,pshold", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_msm_restart_match); + +static struct platform_driver msm_restart_driver = { + .probe = msm_restart_probe, + .driver = { + .name = "msm-restart", + .of_match_table = of_match_ptr(of_msm_restart_match), + }, +}; + +static int __init msm_restart_init(void) +{ + return platform_driver_register(&msm_restart_driver); +} +device_initcall(msm_restart_init); diff --git a/drivers/power/reset/mt6323-poweroff.c b/drivers/power/reset/mt6323-poweroff.c new file mode 100644 index 000000000..d90e76fcb --- /dev/null +++ b/drivers/power/reset/mt6323-poweroff.c @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Power off through MediaTek PMIC + * + * Copyright (C) 2018 MediaTek Inc. + * + * Author: Sean Wang <sean.wang@mediatek.com> + * + */ + +#include <linux/err.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/mfd/mt6397/core.h> +#include <linux/mfd/mt6397/rtc.h> + +struct mt6323_pwrc { + struct device *dev; + struct regmap *regmap; + u32 base; +}; + +static struct mt6323_pwrc *mt_pwrc; + +static void mt6323_do_pwroff(void) +{ + struct mt6323_pwrc *pwrc = mt_pwrc; + unsigned int val; + int ret; + + regmap_write(pwrc->regmap, pwrc->base + RTC_BBPU, RTC_BBPU_KEY); + regmap_write(pwrc->regmap, pwrc->base + RTC_WRTGR_MT6323, 1); + + ret = regmap_read_poll_timeout(pwrc->regmap, + pwrc->base + RTC_BBPU, val, + !(val & RTC_BBPU_CBUSY), + MTK_RTC_POLL_DELAY_US, + MTK_RTC_POLL_TIMEOUT); + if (ret) + dev_err(pwrc->dev, "failed to write BBPU: %d\n", ret); + + /* Wait some time until system down, otherwise, notice with a warn */ + mdelay(1000); + + WARN_ONCE(1, "Unable to power off system\n"); +} + +static int mt6323_pwrc_probe(struct platform_device *pdev) +{ + struct mt6397_chip *mt6397_chip = dev_get_drvdata(pdev->dev.parent); + struct mt6323_pwrc *pwrc; + struct resource *res; + + pwrc = devm_kzalloc(&pdev->dev, sizeof(*pwrc), GFP_KERNEL); + if (!pwrc) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EINVAL; + + pwrc->base = res->start; + pwrc->regmap = mt6397_chip->regmap; + pwrc->dev = &pdev->dev; + mt_pwrc = pwrc; + + pm_power_off = &mt6323_do_pwroff; + + return 0; +} + +static int mt6323_pwrc_remove(struct platform_device *pdev) +{ + if (pm_power_off == &mt6323_do_pwroff) + pm_power_off = NULL; + + return 0; +} + +static const struct of_device_id mt6323_pwrc_dt_match[] = { + { .compatible = "mediatek,mt6323-pwrc" }, + {}, +}; +MODULE_DEVICE_TABLE(of, mt6323_pwrc_dt_match); + +static struct platform_driver mt6323_pwrc_driver = { + .probe = mt6323_pwrc_probe, + .remove = mt6323_pwrc_remove, + .driver = { + .name = "mt6323-pwrc", + .of_match_table = mt6323_pwrc_dt_match, + }, +}; + +module_platform_driver(mt6323_pwrc_driver); + +MODULE_DESCRIPTION("Poweroff driver for MT6323 PMIC"); +MODULE_AUTHOR("Sean Wang <sean.wang@mediatek.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/nvmem-reboot-mode.c b/drivers/power/reset/nvmem-reboot-mode.c new file mode 100644 index 000000000..e229308d4 --- /dev/null +++ b/drivers/power/reset/nvmem-reboot-mode.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (c) Vaisala Oyj. All rights reserved. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/nvmem-consumer.h> +#include <linux/platform_device.h> +#include <linux/reboot-mode.h> + +struct nvmem_reboot_mode { + struct reboot_mode_driver reboot; + struct nvmem_cell *cell; +}; + +static int nvmem_reboot_mode_write(struct reboot_mode_driver *reboot, + unsigned int magic) +{ + int ret; + struct nvmem_reboot_mode *nvmem_rbm; + + nvmem_rbm = container_of(reboot, struct nvmem_reboot_mode, reboot); + + ret = nvmem_cell_write(nvmem_rbm->cell, &magic, sizeof(magic)); + if (ret < 0) + dev_err(reboot->dev, "update reboot mode bits failed\n"); + + return ret; +} + +static int nvmem_reboot_mode_probe(struct platform_device *pdev) +{ + int ret; + struct nvmem_reboot_mode *nvmem_rbm; + + nvmem_rbm = devm_kzalloc(&pdev->dev, sizeof(*nvmem_rbm), GFP_KERNEL); + if (!nvmem_rbm) + return -ENOMEM; + + nvmem_rbm->reboot.dev = &pdev->dev; + nvmem_rbm->reboot.write = nvmem_reboot_mode_write; + + nvmem_rbm->cell = devm_nvmem_cell_get(&pdev->dev, "reboot-mode"); + if (IS_ERR(nvmem_rbm->cell)) { + dev_err(&pdev->dev, "failed to get the nvmem cell reboot-mode\n"); + return PTR_ERR(nvmem_rbm->cell); + } + + ret = devm_reboot_mode_register(&pdev->dev, &nvmem_rbm->reboot); + if (ret) + dev_err(&pdev->dev, "can't register reboot mode\n"); + + return ret; +} + +static const struct of_device_id nvmem_reboot_mode_of_match[] = { + { .compatible = "nvmem-reboot-mode" }, + {} +}; +MODULE_DEVICE_TABLE(of, nvmem_reboot_mode_of_match); + +static struct platform_driver nvmem_reboot_mode_driver = { + .probe = nvmem_reboot_mode_probe, + .driver = { + .name = "nvmem-reboot-mode", + .of_match_table = nvmem_reboot_mode_of_match, + }, +}; +module_platform_driver(nvmem_reboot_mode_driver); + +MODULE_AUTHOR("Nandor Han <nandor.han@vaisala.com>"); +MODULE_DESCRIPTION("NVMEM reboot mode driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/reset/ocelot-reset.c b/drivers/power/reset/ocelot-reset.c new file mode 100644 index 000000000..f74e1dbb4 --- /dev/null +++ b/drivers/power/reset/ocelot-reset.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: (GPL-2.0 OR MIT) +/* + * Microsemi MIPS SoC reset driver + * + * License: Dual MIT/GPL + * Copyright (c) 2017 Microsemi Corporation + */ +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/notifier.h> +#include <linux/mfd/syscon.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +struct reset_props { + const char *syscon; + u32 protect_reg; + u32 vcore_protect; + u32 if_si_owner_bit; +}; + +struct ocelot_reset_context { + void __iomem *base; + struct regmap *cpu_ctrl; + const struct reset_props *props; + struct notifier_block restart_handler; +}; + +#define SOFT_CHIP_RST BIT(0) + +#define ICPU_CFG_CPU_SYSTEM_CTRL_GENERAL_CTRL 0x24 +#define IF_SI_OWNER_MASK GENMASK(1, 0) +#define IF_SI_OWNER_SISL 0 +#define IF_SI_OWNER_SIBM 1 +#define IF_SI_OWNER_SIMC 2 + +static int ocelot_restart_handle(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct ocelot_reset_context *ctx = container_of(this, struct + ocelot_reset_context, + restart_handler); + u32 if_si_owner_bit = ctx->props->if_si_owner_bit; + + /* Make sure the core is not protected from reset */ + regmap_update_bits(ctx->cpu_ctrl, ctx->props->protect_reg, + ctx->props->vcore_protect, 0); + + /* Make the SI back to boot mode */ + regmap_update_bits(ctx->cpu_ctrl, ICPU_CFG_CPU_SYSTEM_CTRL_GENERAL_CTRL, + IF_SI_OWNER_MASK << if_si_owner_bit, + IF_SI_OWNER_SIBM << if_si_owner_bit); + + pr_emerg("Resetting SoC\n"); + + writel(SOFT_CHIP_RST, ctx->base); + + pr_emerg("Unable to restart system\n"); + return NOTIFY_DONE; +} + +static int ocelot_reset_probe(struct platform_device *pdev) +{ + struct ocelot_reset_context *ctx; + struct resource *res; + + struct device *dev = &pdev->dev; + int err; + + ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + ctx->base = devm_ioremap_resource(dev, res); + if (IS_ERR(ctx->base)) + return PTR_ERR(ctx->base); + + ctx->props = device_get_match_data(dev); + + ctx->cpu_ctrl = syscon_regmap_lookup_by_compatible(ctx->props->syscon); + if (IS_ERR(ctx->cpu_ctrl)) { + dev_err(dev, "No syscon map: %s\n", ctx->props->syscon); + return PTR_ERR(ctx->cpu_ctrl); + } + + ctx->restart_handler.notifier_call = ocelot_restart_handle; + ctx->restart_handler.priority = 192; + err = register_restart_handler(&ctx->restart_handler); + if (err) + dev_err(dev, "can't register restart notifier (err=%d)\n", err); + + return err; +} + +static const struct reset_props reset_props_ocelot = { + .syscon = "mscc,ocelot-cpu-syscon", + .protect_reg = 0x20, + .vcore_protect = BIT(2), + .if_si_owner_bit = 4, +}; + +static const struct reset_props reset_props_sparx5 = { + .syscon = "microchip,sparx5-cpu-syscon", + .protect_reg = 0x84, + .vcore_protect = BIT(10), + .if_si_owner_bit = 6, +}; + +static const struct of_device_id ocelot_reset_of_match[] = { + { + .compatible = "mscc,ocelot-chip-reset", + .data = &reset_props_ocelot + }, { + .compatible = "microchip,sparx5-chip-reset", + .data = &reset_props_sparx5 + }, + { /*sentinel*/ } +}; + +static struct platform_driver ocelot_reset_driver = { + .probe = ocelot_reset_probe, + .driver = { + .name = "ocelot-chip-reset", + .of_match_table = ocelot_reset_of_match, + }, +}; +builtin_platform_driver(ocelot_reset_driver); diff --git a/drivers/power/reset/oxnas-restart.c b/drivers/power/reset/oxnas-restart.c new file mode 100644 index 000000000..13090bec0 --- /dev/null +++ b/drivers/power/reset/oxnas-restart.c @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: (GPL-2.0) +/* + * oxnas SoC reset driver + * based on: + * Microsemi MIPS SoC reset driver + * and ox820_assert_system_reset() written by Ma Hajun <mahaijuns@gmail.com> + * + * Copyright (c) 2013 Ma Hajun <mahaijuns@gmail.com> + * Copyright (c) 2017 Microsemi Corporation + * Copyright (c) 2020 Daniel Golle <daniel@makrotopia.org> + */ +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/notifier.h> +#include <linux/mfd/syscon.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +/* bit numbers of reset control register */ +#define OX820_SYS_CTRL_RST_SCU 0 +#define OX820_SYS_CTRL_RST_COPRO 1 +#define OX820_SYS_CTRL_RST_ARM0 2 +#define OX820_SYS_CTRL_RST_ARM1 3 +#define OX820_SYS_CTRL_RST_USBHS 4 +#define OX820_SYS_CTRL_RST_USBHSPHYA 5 +#define OX820_SYS_CTRL_RST_MACA 6 +#define OX820_SYS_CTRL_RST_MAC OX820_SYS_CTRL_RST_MACA +#define OX820_SYS_CTRL_RST_PCIEA 7 +#define OX820_SYS_CTRL_RST_SGDMA 8 +#define OX820_SYS_CTRL_RST_CIPHER 9 +#define OX820_SYS_CTRL_RST_DDR 10 +#define OX820_SYS_CTRL_RST_SATA 11 +#define OX820_SYS_CTRL_RST_SATA_LINK 12 +#define OX820_SYS_CTRL_RST_SATA_PHY 13 +#define OX820_SYS_CTRL_RST_PCIEPHY 14 +#define OX820_SYS_CTRL_RST_STATIC 15 +#define OX820_SYS_CTRL_RST_GPIO 16 +#define OX820_SYS_CTRL_RST_UART1 17 +#define OX820_SYS_CTRL_RST_UART2 18 +#define OX820_SYS_CTRL_RST_MISC 19 +#define OX820_SYS_CTRL_RST_I2S 20 +#define OX820_SYS_CTRL_RST_SD 21 +#define OX820_SYS_CTRL_RST_MACB 22 +#define OX820_SYS_CTRL_RST_PCIEB 23 +#define OX820_SYS_CTRL_RST_VIDEO 24 +#define OX820_SYS_CTRL_RST_DDR_PHY 25 +#define OX820_SYS_CTRL_RST_USBHSPHYB 26 +#define OX820_SYS_CTRL_RST_USBDEV 27 +#define OX820_SYS_CTRL_RST_ARMDBG 29 +#define OX820_SYS_CTRL_RST_PLLA 30 +#define OX820_SYS_CTRL_RST_PLLB 31 + +/* bit numbers of clock control register */ +#define OX820_SYS_CTRL_CLK_COPRO 0 +#define OX820_SYS_CTRL_CLK_DMA 1 +#define OX820_SYS_CTRL_CLK_CIPHER 2 +#define OX820_SYS_CTRL_CLK_SD 3 +#define OX820_SYS_CTRL_CLK_SATA 4 +#define OX820_SYS_CTRL_CLK_I2S 5 +#define OX820_SYS_CTRL_CLK_USBHS 6 +#define OX820_SYS_CTRL_CLK_MACA 7 +#define OX820_SYS_CTRL_CLK_MAC OX820_SYS_CTRL_CLK_MACA +#define OX820_SYS_CTRL_CLK_PCIEA 8 +#define OX820_SYS_CTRL_CLK_STATIC 9 +#define OX820_SYS_CTRL_CLK_MACB 10 +#define OX820_SYS_CTRL_CLK_PCIEB 11 +#define OX820_SYS_CTRL_CLK_REF600 12 +#define OX820_SYS_CTRL_CLK_USBDEV 13 +#define OX820_SYS_CTRL_CLK_DDR 14 +#define OX820_SYS_CTRL_CLK_DDRPHY 15 +#define OX820_SYS_CTRL_CLK_DDRCK 16 + +/* Regmap offsets */ +#define OX820_CLK_SET_REGOFFSET 0x2c +#define OX820_CLK_CLR_REGOFFSET 0x30 +#define OX820_RST_SET_REGOFFSET 0x34 +#define OX820_RST_CLR_REGOFFSET 0x38 +#define OX820_SECONDARY_SEL_REGOFFSET 0x14 +#define OX820_TERTIARY_SEL_REGOFFSET 0x8c +#define OX820_QUATERNARY_SEL_REGOFFSET 0x94 +#define OX820_DEBUG_SEL_REGOFFSET 0x9c +#define OX820_ALTERNATIVE_SEL_REGOFFSET 0xa4 +#define OX820_PULLUP_SEL_REGOFFSET 0xac +#define OX820_SEC_SECONDARY_SEL_REGOFFSET 0x100014 +#define OX820_SEC_TERTIARY_SEL_REGOFFSET 0x10008c +#define OX820_SEC_QUATERNARY_SEL_REGOFFSET 0x100094 +#define OX820_SEC_DEBUG_SEL_REGOFFSET 0x10009c +#define OX820_SEC_ALTERNATIVE_SEL_REGOFFSET 0x1000a4 +#define OX820_SEC_PULLUP_SEL_REGOFFSET 0x1000ac + +struct oxnas_restart_context { + struct regmap *sys_ctrl; + struct notifier_block restart_handler; +}; + +static int ox820_restart_handle(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct oxnas_restart_context *ctx = container_of(this, struct + oxnas_restart_context, + restart_handler); + u32 value; + + /* + * Assert reset to cores as per power on defaults + * Don't touch the DDR interface as things will come to an impromptu + * stop NB Possibly should be asserting reset for PLLB, but there are + * timing concerns here according to the docs + */ + value = BIT(OX820_SYS_CTRL_RST_COPRO) | + BIT(OX820_SYS_CTRL_RST_USBHS) | + BIT(OX820_SYS_CTRL_RST_USBHSPHYA) | + BIT(OX820_SYS_CTRL_RST_MACA) | + BIT(OX820_SYS_CTRL_RST_PCIEA) | + BIT(OX820_SYS_CTRL_RST_SGDMA) | + BIT(OX820_SYS_CTRL_RST_CIPHER) | + BIT(OX820_SYS_CTRL_RST_SATA) | + BIT(OX820_SYS_CTRL_RST_SATA_LINK) | + BIT(OX820_SYS_CTRL_RST_SATA_PHY) | + BIT(OX820_SYS_CTRL_RST_PCIEPHY) | + BIT(OX820_SYS_CTRL_RST_STATIC) | + BIT(OX820_SYS_CTRL_RST_UART1) | + BIT(OX820_SYS_CTRL_RST_UART2) | + BIT(OX820_SYS_CTRL_RST_MISC) | + BIT(OX820_SYS_CTRL_RST_I2S) | + BIT(OX820_SYS_CTRL_RST_SD) | + BIT(OX820_SYS_CTRL_RST_MACB) | + BIT(OX820_SYS_CTRL_RST_PCIEB) | + BIT(OX820_SYS_CTRL_RST_VIDEO) | + BIT(OX820_SYS_CTRL_RST_USBHSPHYB) | + BIT(OX820_SYS_CTRL_RST_USBDEV); + + regmap_write(ctx->sys_ctrl, OX820_RST_SET_REGOFFSET, value); + + /* Release reset to cores as per power on defaults */ + regmap_write(ctx->sys_ctrl, OX820_RST_CLR_REGOFFSET, + BIT(OX820_SYS_CTRL_RST_GPIO)); + + /* + * Disable clocks to cores as per power-on defaults - must leave DDR + * related clocks enabled otherwise we'll stop rather abruptly. + */ + value = BIT(OX820_SYS_CTRL_CLK_COPRO) | + BIT(OX820_SYS_CTRL_CLK_DMA) | + BIT(OX820_SYS_CTRL_CLK_CIPHER) | + BIT(OX820_SYS_CTRL_CLK_SD) | + BIT(OX820_SYS_CTRL_CLK_SATA) | + BIT(OX820_SYS_CTRL_CLK_I2S) | + BIT(OX820_SYS_CTRL_CLK_USBHS) | + BIT(OX820_SYS_CTRL_CLK_MAC) | + BIT(OX820_SYS_CTRL_CLK_PCIEA) | + BIT(OX820_SYS_CTRL_CLK_STATIC) | + BIT(OX820_SYS_CTRL_CLK_MACB) | + BIT(OX820_SYS_CTRL_CLK_PCIEB) | + BIT(OX820_SYS_CTRL_CLK_REF600) | + BIT(OX820_SYS_CTRL_CLK_USBDEV); + + regmap_write(ctx->sys_ctrl, OX820_CLK_CLR_REGOFFSET, value); + + /* Enable clocks to cores as per power-on defaults */ + + /* Set sys-control pin mux'ing as per power-on defaults */ + regmap_write(ctx->sys_ctrl, OX820_SECONDARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_TERTIARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_QUATERNARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_DEBUG_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_ALTERNATIVE_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_PULLUP_SEL_REGOFFSET, 0); + + regmap_write(ctx->sys_ctrl, OX820_SEC_SECONDARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_SEC_TERTIARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_SEC_QUATERNARY_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_SEC_DEBUG_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_SEC_ALTERNATIVE_SEL_REGOFFSET, 0); + regmap_write(ctx->sys_ctrl, OX820_SEC_PULLUP_SEL_REGOFFSET, 0); + + /* + * No need to save any state, as the ROM loader can determine whether + * reset is due to power cycling or programatic action, just hit the + * (self-clearing) CPU reset bit of the block reset register + */ + value = + BIT(OX820_SYS_CTRL_RST_SCU) | + BIT(OX820_SYS_CTRL_RST_ARM0) | + BIT(OX820_SYS_CTRL_RST_ARM1); + + regmap_write(ctx->sys_ctrl, OX820_RST_SET_REGOFFSET, value); + + pr_emerg("Unable to restart system\n"); + return NOTIFY_DONE; +} + +static int ox820_restart_probe(struct platform_device *pdev) +{ + struct oxnas_restart_context *ctx; + struct regmap *sys_ctrl; + struct device *dev = &pdev->dev; + int err = 0; + + sys_ctrl = syscon_node_to_regmap(pdev->dev.of_node); + if (IS_ERR(sys_ctrl)) + return PTR_ERR(sys_ctrl); + + ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + ctx->sys_ctrl = sys_ctrl; + ctx->restart_handler.notifier_call = ox820_restart_handle; + ctx->restart_handler.priority = 192; + err = register_restart_handler(&ctx->restart_handler); + if (err) + dev_err(dev, "can't register restart notifier (err=%d)\n", err); + + return err; +} + +static const struct of_device_id ox820_restart_of_match[] = { + { .compatible = "oxsemi,ox820-sys-ctrl" }, + {} +}; + +static struct platform_driver ox820_restart_driver = { + .probe = ox820_restart_probe, + .driver = { + .name = "ox820-chip-reset", + .of_match_table = ox820_restart_of_match, + }, +}; +builtin_platform_driver(ox820_restart_driver); diff --git a/drivers/power/reset/piix4-poweroff.c b/drivers/power/reset/piix4-poweroff.c new file mode 100644 index 000000000..7f308292d --- /dev/null +++ b/drivers/power/reset/piix4-poweroff.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2016 Imagination Technologies + * Author: Paul Burton <paul.burton@mips.com> + */ + +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/pci.h> +#include <linux/pm.h> + +static struct pci_dev *pm_dev; +static resource_size_t io_offset; + +enum piix4_pm_io_reg { + PIIX4_FUNC3IO_PMSTS = 0x00, +#define PIIX4_FUNC3IO_PMSTS_PWRBTN_STS BIT(8) + PIIX4_FUNC3IO_PMCNTRL = 0x04, +#define PIIX4_FUNC3IO_PMCNTRL_SUS_EN BIT(13) +#define PIIX4_FUNC3IO_PMCNTRL_SUS_TYP_SOFF (0x0 << 10) +}; + +#define PIIX4_SUSPEND_MAGIC 0x00120002 + +static const int piix4_pm_io_region = PCI_BRIDGE_RESOURCES; + +static void piix4_poweroff(void) +{ + int spec_devid; + u16 sts; + + /* Ensure the power button status is clear */ + while (1) { + sts = inw(io_offset + PIIX4_FUNC3IO_PMSTS); + if (!(sts & PIIX4_FUNC3IO_PMSTS_PWRBTN_STS)) + break; + outw(sts, io_offset + PIIX4_FUNC3IO_PMSTS); + } + + /* Enable entry to suspend */ + outw(PIIX4_FUNC3IO_PMCNTRL_SUS_TYP_SOFF | PIIX4_FUNC3IO_PMCNTRL_SUS_EN, + io_offset + PIIX4_FUNC3IO_PMCNTRL); + + /* If the special cycle occurs too soon this doesn't work... */ + mdelay(10); + + /* + * The PIIX4 will enter the suspend state only after seeing a special + * cycle with the correct magic data on the PCI bus. Generate that + * cycle now. + */ + spec_devid = PCI_DEVID(0, PCI_DEVFN(0x1f, 0x7)); + pci_bus_write_config_dword(pm_dev->bus, spec_devid, 0, + PIIX4_SUSPEND_MAGIC); + + /* Give the system some time to power down, then error */ + mdelay(1000); + pr_emerg("Unable to poweroff system\n"); +} + +static int piix4_poweroff_probe(struct pci_dev *dev, + const struct pci_device_id *id) +{ + int res; + + if (pm_dev) + return -EINVAL; + + /* Request access to the PIIX4 PM IO registers */ + res = pci_request_region(dev, piix4_pm_io_region, + "PIIX4 PM IO registers"); + if (res) { + dev_err(&dev->dev, "failed to request PM IO registers: %d\n", + res); + return res; + } + + pm_dev = dev; + io_offset = pci_resource_start(dev, piix4_pm_io_region); + pm_power_off = piix4_poweroff; + + return 0; +} + +static void piix4_poweroff_remove(struct pci_dev *dev) +{ + if (pm_power_off == piix4_poweroff) + pm_power_off = NULL; + + pci_release_region(dev, piix4_pm_io_region); + pm_dev = NULL; +} + +static const struct pci_device_id piix4_poweroff_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82371AB_3) }, + { 0 }, +}; + +static struct pci_driver piix4_poweroff_driver = { + .name = "piix4-poweroff", + .id_table = piix4_poweroff_ids, + .probe = piix4_poweroff_probe, + .remove = piix4_poweroff_remove, +}; + +module_pci_driver(piix4_poweroff_driver); +MODULE_AUTHOR("Paul Burton <paul.burton@mips.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/reset/qcom-pon.c b/drivers/power/reset/qcom-pon.c new file mode 100644 index 000000000..4a688741a --- /dev/null +++ b/drivers/power/reset/qcom-pon.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2017-18 Linaro Limited + +#include <linux/delay.h> +#include <linux/errno.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/reboot-mode.h> +#include <linux/regmap.h> + +#define PON_SOFT_RB_SPARE 0x8f + +#define GEN1_REASON_SHIFT 2 +#define GEN2_REASON_SHIFT 1 + +struct pm8916_pon { + struct device *dev; + struct regmap *regmap; + u32 baseaddr; + struct reboot_mode_driver reboot_mode; + long reason_shift; +}; + +static int pm8916_reboot_mode_write(struct reboot_mode_driver *reboot, + unsigned int magic) +{ + struct pm8916_pon *pon = container_of + (reboot, struct pm8916_pon, reboot_mode); + int ret; + + ret = regmap_update_bits(pon->regmap, + pon->baseaddr + PON_SOFT_RB_SPARE, + GENMASK(7, pon->reason_shift), + magic << pon->reason_shift); + if (ret < 0) + dev_err(pon->dev, "update reboot mode bits failed\n"); + + return ret; +} + +static int pm8916_pon_probe(struct platform_device *pdev) +{ + struct pm8916_pon *pon; + int error; + + pon = devm_kzalloc(&pdev->dev, sizeof(*pon), GFP_KERNEL); + if (!pon) + return -ENOMEM; + + pon->dev = &pdev->dev; + + pon->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!pon->regmap) { + dev_err(&pdev->dev, "failed to locate regmap\n"); + return -ENODEV; + } + + error = of_property_read_u32(pdev->dev.of_node, "reg", + &pon->baseaddr); + if (error) + return error; + + pon->reboot_mode.dev = &pdev->dev; + pon->reason_shift = (long)of_device_get_match_data(&pdev->dev); + pon->reboot_mode.write = pm8916_reboot_mode_write; + error = devm_reboot_mode_register(&pdev->dev, &pon->reboot_mode); + if (error) { + dev_err(&pdev->dev, "can't register reboot mode\n"); + return error; + } + + platform_set_drvdata(pdev, pon); + + return devm_of_platform_populate(&pdev->dev); +} + +static const struct of_device_id pm8916_pon_id_table[] = { + { .compatible = "qcom,pm8916-pon", .data = (void *)GEN1_REASON_SHIFT }, + { .compatible = "qcom,pms405-pon", .data = (void *)GEN1_REASON_SHIFT }, + { .compatible = "qcom,pm8998-pon", .data = (void *)GEN2_REASON_SHIFT }, + { } +}; +MODULE_DEVICE_TABLE(of, pm8916_pon_id_table); + +static struct platform_driver pm8916_pon_driver = { + .probe = pm8916_pon_probe, + .driver = { + .name = "pm8916-pon", + .of_match_table = of_match_ptr(pm8916_pon_id_table), + }, +}; +module_platform_driver(pm8916_pon_driver); + +MODULE_DESCRIPTION("pm8916 Power On driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/qnap-poweroff.c b/drivers/power/reset/qnap-poweroff.c new file mode 100644 index 000000000..52b7dc61d --- /dev/null +++ b/drivers/power/reset/qnap-poweroff.c @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * QNAP Turbo NAS Board power off. Can also be used on Synology devices. + * + * Copyright (C) 2012 Andrew Lunn <andrew@lunn.ch> + * + * Based on the code from: + * + * Copyright (C) 2009 Martin Michlmayr <tbm@cyrius.com> + * Copyright (C) 2008 Byron Bradley <byron.bbradley@gmail.com> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/serial_reg.h> +#include <linux/kallsyms.h> +#include <linux/of.h> +#include <linux/io.h> +#include <linux/clk.h> + +#define UART1_REG(x) (base + ((UART_##x) << 2)) + +struct power_off_cfg { + u32 baud; + char cmd; +}; + +static const struct power_off_cfg qnap_power_off_cfg = { + .baud = 19200, + .cmd = 'A', +}; + +static const struct power_off_cfg synology_power_off_cfg = { + .baud = 9600, + .cmd = '1', +}; + +static const struct of_device_id qnap_power_off_of_match_table[] = { + { .compatible = "qnap,power-off", + .data = &qnap_power_off_cfg, + }, + { .compatible = "synology,power-off", + .data = &synology_power_off_cfg, + }, + {} +}; +MODULE_DEVICE_TABLE(of, qnap_power_off_of_match_table); + +static void __iomem *base; +static unsigned long tclk; +static const struct power_off_cfg *cfg; + +static void qnap_power_off(void) +{ + const unsigned divisor = ((tclk + (8 * cfg->baud)) / (16 * cfg->baud)); + + pr_err("%s: triggering power-off...\n", __func__); + + /* hijack UART1 and reset into sane state */ + writel(0x83, UART1_REG(LCR)); + writel(divisor & 0xff, UART1_REG(DLL)); + writel((divisor >> 8) & 0xff, UART1_REG(DLM)); + writel(0x03, UART1_REG(LCR)); + writel(0x00, UART1_REG(IER)); + writel(0x00, UART1_REG(FCR)); + writel(0x00, UART1_REG(MCR)); + + /* send the power-off command to PIC */ + writel(cfg->cmd, UART1_REG(TX)); +} + +static int qnap_power_off_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct resource *res; + struct clk *clk; + char symname[KSYM_NAME_LEN]; + + const struct of_device_id *match = + of_match_node(qnap_power_off_of_match_table, np); + cfg = match->data; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(&pdev->dev, "Missing resource"); + return -EINVAL; + } + + base = devm_ioremap(&pdev->dev, res->start, resource_size(res)); + if (!base) { + dev_err(&pdev->dev, "Unable to map resource"); + return -EINVAL; + } + + /* We need to know tclk in order to calculate the UART divisor */ + clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(clk)) { + dev_err(&pdev->dev, "Clk missing"); + return PTR_ERR(clk); + } + + tclk = clk_get_rate(clk); + + /* Check that nothing else has already setup a handler */ + if (pm_power_off) { + lookup_symbol_name((ulong)pm_power_off, symname); + dev_err(&pdev->dev, + "pm_power_off already claimed %p %s", + pm_power_off, symname); + return -EBUSY; + } + pm_power_off = qnap_power_off; + + return 0; +} + +static int qnap_power_off_remove(struct platform_device *pdev) +{ + pm_power_off = NULL; + return 0; +} + +static struct platform_driver qnap_power_off_driver = { + .probe = qnap_power_off_probe, + .remove = qnap_power_off_remove, + .driver = { + .name = "qnap_power_off", + .of_match_table = of_match_ptr(qnap_power_off_of_match_table), + }, +}; +module_platform_driver(qnap_power_off_driver); + +MODULE_AUTHOR("Andrew Lunn <andrew@lunn.ch>"); +MODULE_DESCRIPTION("QNAP Power off driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/reboot-mode.c b/drivers/power/reset/reboot-mode.c new file mode 100644 index 000000000..b4076b10b --- /dev/null +++ b/drivers/power/reset/reboot-mode.c @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2016, Fuzhou Rockchip Electronics Co., Ltd + */ + +#include <linux/device.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/reboot.h> +#include <linux/reboot-mode.h> + +#define PREFIX "mode-" + +struct mode_info { + const char *mode; + u32 magic; + struct list_head list; +}; + +static unsigned int get_reboot_mode_magic(struct reboot_mode_driver *reboot, + const char *cmd) +{ + const char *normal = "normal"; + int magic = 0; + struct mode_info *info; + + if (!cmd) + cmd = normal; + + list_for_each_entry(info, &reboot->head, list) { + if (!strcmp(info->mode, cmd)) { + magic = info->magic; + break; + } + } + + return magic; +} + +static int reboot_mode_notify(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct reboot_mode_driver *reboot; + unsigned int magic; + + reboot = container_of(this, struct reboot_mode_driver, reboot_notifier); + magic = get_reboot_mode_magic(reboot, cmd); + if (magic) + reboot->write(reboot, magic); + + return NOTIFY_DONE; +} + +/** + * reboot_mode_register - register a reboot mode driver + * @reboot: reboot mode driver + * + * Returns: 0 on success or a negative error code on failure. + */ +int reboot_mode_register(struct reboot_mode_driver *reboot) +{ + struct mode_info *info; + struct property *prop; + struct device_node *np = reboot->dev->of_node; + size_t len = strlen(PREFIX); + int ret; + + INIT_LIST_HEAD(&reboot->head); + + for_each_property_of_node(np, prop) { + if (strncmp(prop->name, PREFIX, len)) + continue; + + info = devm_kzalloc(reboot->dev, sizeof(*info), GFP_KERNEL); + if (!info) { + ret = -ENOMEM; + goto error; + } + + if (of_property_read_u32(np, prop->name, &info->magic)) { + dev_err(reboot->dev, "reboot mode %s without magic number\n", + info->mode); + devm_kfree(reboot->dev, info); + continue; + } + + info->mode = kstrdup_const(prop->name + len, GFP_KERNEL); + if (!info->mode) { + ret = -ENOMEM; + goto error; + } else if (info->mode[0] == '\0') { + kfree_const(info->mode); + ret = -EINVAL; + dev_err(reboot->dev, "invalid mode name(%s): too short!\n", + prop->name); + goto error; + } + + list_add_tail(&info->list, &reboot->head); + } + + reboot->reboot_notifier.notifier_call = reboot_mode_notify; + register_reboot_notifier(&reboot->reboot_notifier); + + return 0; + +error: + list_for_each_entry(info, &reboot->head, list) + kfree_const(info->mode); + + return ret; +} +EXPORT_SYMBOL_GPL(reboot_mode_register); + +/** + * reboot_mode_unregister - unregister a reboot mode driver + * @reboot: reboot mode driver + */ +int reboot_mode_unregister(struct reboot_mode_driver *reboot) +{ + struct mode_info *info; + + unregister_reboot_notifier(&reboot->reboot_notifier); + + list_for_each_entry(info, &reboot->head, list) + kfree_const(info->mode); + + return 0; +} +EXPORT_SYMBOL_GPL(reboot_mode_unregister); + +static void devm_reboot_mode_release(struct device *dev, void *res) +{ + reboot_mode_unregister(*(struct reboot_mode_driver **)res); +} + +/** + * devm_reboot_mode_register() - resource managed reboot_mode_register() + * @dev: device to associate this resource with + * @reboot: reboot mode driver + * + * Returns: 0 on success or a negative error code on failure. + */ +int devm_reboot_mode_register(struct device *dev, + struct reboot_mode_driver *reboot) +{ + struct reboot_mode_driver **dr; + int rc; + + dr = devres_alloc(devm_reboot_mode_release, sizeof(*dr), GFP_KERNEL); + if (!dr) + return -ENOMEM; + + rc = reboot_mode_register(reboot); + if (rc) { + devres_free(dr); + return rc; + } + + *dr = reboot; + devres_add(dev, dr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_reboot_mode_register); + +static int devm_reboot_mode_match(struct device *dev, void *res, void *data) +{ + struct reboot_mode_driver **p = res; + + if (WARN_ON(!p || !*p)) + return 0; + + return *p == data; +} + +/** + * devm_reboot_mode_unregister() - resource managed reboot_mode_unregister() + * @dev: device to associate this resource with + * @reboot: reboot mode driver + */ +void devm_reboot_mode_unregister(struct device *dev, + struct reboot_mode_driver *reboot) +{ + WARN_ON(devres_release(dev, + devm_reboot_mode_release, + devm_reboot_mode_match, reboot)); +} +EXPORT_SYMBOL_GPL(devm_reboot_mode_unregister); + +MODULE_AUTHOR("Andy Yan <andy.yan@rock-chips.com>"); +MODULE_DESCRIPTION("System reboot mode core library"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/restart-poweroff.c b/drivers/power/reset/restart-poweroff.c new file mode 100644 index 000000000..d233daa58 --- /dev/null +++ b/drivers/power/reset/restart-poweroff.c @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Power off by restarting and let u-boot keep hold of the machine + * until the user presses a button for example. + * + * Andrew Lunn <andrew@lunn.ch> + * + * Copyright (C) 2012 Andrew Lunn + */ +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/of_platform.h> +#include <linux/module.h> +#include <linux/reboot.h> + +static void restart_poweroff_do_poweroff(void) +{ + reboot_mode = REBOOT_HARD; + machine_restart(NULL); +} + +static int restart_poweroff_probe(struct platform_device *pdev) +{ + /* If a pm_power_off function has already been added, leave it alone */ + if (pm_power_off != NULL) { + dev_err(&pdev->dev, + "pm_power_off function already registered"); + return -EBUSY; + } + + pm_power_off = &restart_poweroff_do_poweroff; + return 0; +} + +static int restart_poweroff_remove(struct platform_device *pdev) +{ + if (pm_power_off == &restart_poweroff_do_poweroff) + pm_power_off = NULL; + + return 0; +} + +static const struct of_device_id of_restart_poweroff_match[] = { + { .compatible = "restart-poweroff", }, + {}, +}; + +static struct platform_driver restart_poweroff_driver = { + .probe = restart_poweroff_probe, + .remove = restart_poweroff_remove, + .driver = { + .name = "poweroff-restart", + .of_match_table = of_restart_poweroff_match, + }, +}; +module_platform_driver(restart_poweroff_driver); + +MODULE_AUTHOR("Andrew Lunn <andrew@lunn.ch"); +MODULE_DESCRIPTION("restart poweroff driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:poweroff-restart"); diff --git a/drivers/power/reset/rmobile-reset.c b/drivers/power/reset/rmobile-reset.c new file mode 100644 index 000000000..bd3b39655 --- /dev/null +++ b/drivers/power/reset/rmobile-reset.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Renesas R-Mobile Reset Driver + * + * Copyright (C) 2014 Glider bvba + */ + +#include <linux/io.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/printk.h> +#include <linux/reboot.h> + +/* SYSC Register Bank 2 */ +#define RESCNT2 0x20 /* Reset Control Register 2 */ + +/* Reset Control Register 2 */ +#define RESCNT2_PRES 0x80000000 /* Soft power-on reset */ + +static void __iomem *sysc_base2; + +static int rmobile_reset_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + pr_debug("%s %lu\n", __func__, mode); + + /* Let's assume we have acquired the HPB semaphore */ + writel(RESCNT2_PRES, sysc_base2 + RESCNT2); + + return NOTIFY_DONE; +} + +static struct notifier_block rmobile_reset_nb = { + .notifier_call = rmobile_reset_handler, + .priority = 192, +}; + +static int rmobile_reset_probe(struct platform_device *pdev) +{ + int error; + + sysc_base2 = of_iomap(pdev->dev.of_node, 1); + if (!sysc_base2) + return -ENODEV; + + error = register_restart_handler(&rmobile_reset_nb); + if (error) { + dev_err(&pdev->dev, + "cannot register restart handler (err=%d)\n", error); + goto fail_unmap; + } + + return 0; + +fail_unmap: + iounmap(sysc_base2); + return error; +} + +static int rmobile_reset_remove(struct platform_device *pdev) +{ + unregister_restart_handler(&rmobile_reset_nb); + iounmap(sysc_base2); + return 0; +} + +static const struct of_device_id rmobile_reset_of_match[] = { + { .compatible = "renesas,sysc-rmobile", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, rmobile_reset_of_match); + +static struct platform_driver rmobile_reset_driver = { + .probe = rmobile_reset_probe, + .remove = rmobile_reset_remove, + .driver = { + .name = "rmobile_reset", + .of_match_table = rmobile_reset_of_match, + }, +}; + +module_platform_driver(rmobile_reset_driver); + +MODULE_DESCRIPTION("Renesas R-Mobile Reset Driver"); +MODULE_AUTHOR("Geert Uytterhoeven <geert+renesas@glider.be>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/sc27xx-poweroff.c b/drivers/power/reset/sc27xx-poweroff.c new file mode 100644 index 000000000..90287c319 --- /dev/null +++ b/drivers/power/reset/sc27xx-poweroff.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2018 Spreadtrum Communications Inc. + * Copyright (C) 2018 Linaro Ltd. + */ + +#include <linux/cpu.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/regmap.h> +#include <linux/syscore_ops.h> + +#define SC27XX_PWR_PD_HW 0xc2c +#define SC27XX_PWR_OFF_EN BIT(0) +#define SC27XX_SLP_CTRL 0xdf0 +#define SC27XX_LDO_XTL_EN BIT(3) + +static struct regmap *regmap; + +/* + * On Spreadtrum platform, we need power off system through external SC27xx + * series PMICs, and it is one similar SPI bus mapped by regmap to access PMIC, + * which is not fast io access. + * + * So before stopping other cores, we need release other cores' resource by + * taking cpus down to avoid racing regmap or spi mutex lock when poweroff + * system through PMIC. + */ +static void sc27xx_poweroff_shutdown(void) +{ +#ifdef CONFIG_HOTPLUG_CPU + int cpu; + + for_each_online_cpu(cpu) { + if (cpu != smp_processor_id()) + remove_cpu(cpu); + } +#endif +} + +static struct syscore_ops poweroff_syscore_ops = { + .shutdown = sc27xx_poweroff_shutdown, +}; + +static void sc27xx_poweroff_do_poweroff(void) +{ + /* Disable the external subsys connection's power firstly */ + regmap_write(regmap, SC27XX_SLP_CTRL, SC27XX_LDO_XTL_EN); + + regmap_write(regmap, SC27XX_PWR_PD_HW, SC27XX_PWR_OFF_EN); +} + +static int sc27xx_poweroff_probe(struct platform_device *pdev) +{ + if (regmap) + return -EINVAL; + + regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!regmap) + return -ENODEV; + + pm_power_off = sc27xx_poweroff_do_poweroff; + register_syscore_ops(&poweroff_syscore_ops); + return 0; +} + +static struct platform_driver sc27xx_poweroff_driver = { + .probe = sc27xx_poweroff_probe, + .driver = { + .name = "sc27xx-poweroff", + }, +}; +module_platform_driver(sc27xx_poweroff_driver); + +MODULE_DESCRIPTION("Power off driver for SC27XX PMIC Device"); +MODULE_AUTHOR("Baolin Wang <baolin.wang@unisoc.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/st-poweroff.c b/drivers/power/reset/st-poweroff.c new file mode 100644 index 000000000..5ccaacffa --- /dev/null +++ b/drivers/power/reset/st-poweroff.c @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2014 STMicroelectronics + * + * Power off Restart driver, used in STMicroelectronics devices. + * + * Author: Christophe Kerello <christophe.kerello@st.com> + */ + +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/mfd/syscon.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +struct reset_syscfg { + struct regmap *regmap; + /* syscfg used for reset */ + unsigned int offset_rst; + unsigned int mask_rst; + /* syscfg used for unmask the reset */ + unsigned int offset_rst_msk; + unsigned int mask_rst_msk; +}; + +/* STiH407 */ +#define STIH407_SYSCFG_4000 0x0 +#define STIH407_SYSCFG_4008 0x20 + +static struct reset_syscfg stih407_reset = { + .offset_rst = STIH407_SYSCFG_4000, + .mask_rst = BIT(0), + .offset_rst_msk = STIH407_SYSCFG_4008, + .mask_rst_msk = BIT(0) +}; + + +static struct reset_syscfg *st_restart_syscfg; + +static int st_restart(struct notifier_block *this, unsigned long mode, + void *cmd) +{ + /* reset syscfg updated */ + regmap_update_bits(st_restart_syscfg->regmap, + st_restart_syscfg->offset_rst, + st_restart_syscfg->mask_rst, + 0); + + /* unmask the reset */ + regmap_update_bits(st_restart_syscfg->regmap, + st_restart_syscfg->offset_rst_msk, + st_restart_syscfg->mask_rst_msk, + 0); + + return NOTIFY_DONE; +} + +static struct notifier_block st_restart_nb = { + .notifier_call = st_restart, + .priority = 192, +}; + +static const struct of_device_id st_reset_of_match[] = { + { + .compatible = "st,stih407-restart", + .data = (void *)&stih407_reset, + }, + {} +}; + +static int st_reset_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + const struct of_device_id *match; + struct device *dev = &pdev->dev; + + match = of_match_device(st_reset_of_match, dev); + if (!match) + return -ENODEV; + + st_restart_syscfg = (struct reset_syscfg *)match->data; + + st_restart_syscfg->regmap = + syscon_regmap_lookup_by_phandle(np, "st,syscfg"); + if (IS_ERR(st_restart_syscfg->regmap)) { + dev_err(dev, "No syscfg phandle specified\n"); + return PTR_ERR(st_restart_syscfg->regmap); + } + + return register_restart_handler(&st_restart_nb); +} + +static struct platform_driver st_reset_driver = { + .probe = st_reset_probe, + .driver = { + .name = "st_reset", + .of_match_table = st_reset_of_match, + }, +}; + +static int __init st_reset_init(void) +{ + return platform_driver_register(&st_reset_driver); +} + +device_initcall(st_reset_init); + +MODULE_AUTHOR("Christophe Kerello <christophe.kerello@st.com>"); +MODULE_DESCRIPTION("STMicroelectronics Power off Restart driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/syscon-poweroff.c b/drivers/power/reset/syscon-poweroff.c new file mode 100644 index 000000000..4d6923b10 --- /dev/null +++ b/drivers/power/reset/syscon-poweroff.c @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic Syscon Poweroff Driver + * + * Copyright (c) 2015, National Instruments Corp. + * Author: Moritz Fischer <moritz.fischer@ettus.com> + */ + +#include <linux/kallsyms.h> +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/notifier.h> +#include <linux/mfd/syscon.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/regmap.h> + +static struct regmap *map; +static u32 offset; +static u32 value; +static u32 mask; + +static void syscon_poweroff(void) +{ + /* Issue the poweroff */ + regmap_update_bits(map, offset, mask, value); + + mdelay(1000); + + pr_emerg("Unable to poweroff system\n"); +} + +static int syscon_poweroff_probe(struct platform_device *pdev) +{ + char symname[KSYM_NAME_LEN]; + int mask_err, value_err; + + map = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "regmap"); + if (IS_ERR(map)) { + dev_err(&pdev->dev, "unable to get syscon"); + return PTR_ERR(map); + } + + if (of_property_read_u32(pdev->dev.of_node, "offset", &offset)) { + dev_err(&pdev->dev, "unable to read 'offset'"); + return -EINVAL; + } + + value_err = of_property_read_u32(pdev->dev.of_node, "value", &value); + mask_err = of_property_read_u32(pdev->dev.of_node, "mask", &mask); + if (value_err && mask_err) { + dev_err(&pdev->dev, "unable to read 'value' and 'mask'"); + return -EINVAL; + } + + if (value_err) { + /* support old binding */ + value = mask; + mask = 0xFFFFFFFF; + } else if (mask_err) { + /* support value without mask*/ + mask = 0xFFFFFFFF; + } + + if (pm_power_off) { + lookup_symbol_name((ulong)pm_power_off, symname); + dev_err(&pdev->dev, + "pm_power_off already claimed %p %s", + pm_power_off, symname); + return -EBUSY; + } + + pm_power_off = syscon_poweroff; + + return 0; +} + +static int syscon_poweroff_remove(struct platform_device *pdev) +{ + if (pm_power_off == syscon_poweroff) + pm_power_off = NULL; + + return 0; +} + +static const struct of_device_id syscon_poweroff_of_match[] = { + { .compatible = "syscon-poweroff" }, + {} +}; + +static struct platform_driver syscon_poweroff_driver = { + .probe = syscon_poweroff_probe, + .remove = syscon_poweroff_remove, + .driver = { + .name = "syscon-poweroff", + .of_match_table = syscon_poweroff_of_match, + }, +}; + +static int __init syscon_poweroff_register(void) +{ + return platform_driver_register(&syscon_poweroff_driver); +} +device_initcall(syscon_poweroff_register); diff --git a/drivers/power/reset/syscon-reboot-mode.c b/drivers/power/reset/syscon-reboot-mode.c new file mode 100644 index 000000000..e0772c9f7 --- /dev/null +++ b/drivers/power/reset/syscon-reboot-mode.c @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2016, Fuzhou Rockchip Electronics Co., Ltd + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/regmap.h> +#include <linux/mfd/syscon.h> +#include <linux/reboot-mode.h> + +struct syscon_reboot_mode { + struct regmap *map; + struct reboot_mode_driver reboot; + u32 offset; + u32 mask; +}; + +static int syscon_reboot_mode_write(struct reboot_mode_driver *reboot, + unsigned int magic) +{ + struct syscon_reboot_mode *syscon_rbm; + int ret; + + syscon_rbm = container_of(reboot, struct syscon_reboot_mode, reboot); + + ret = regmap_update_bits(syscon_rbm->map, syscon_rbm->offset, + syscon_rbm->mask, magic); + if (ret < 0) + dev_err(reboot->dev, "update reboot mode bits failed\n"); + + return ret; +} + +static int syscon_reboot_mode_probe(struct platform_device *pdev) +{ + int ret; + struct syscon_reboot_mode *syscon_rbm; + + syscon_rbm = devm_kzalloc(&pdev->dev, sizeof(*syscon_rbm), GFP_KERNEL); + if (!syscon_rbm) + return -ENOMEM; + + syscon_rbm->reboot.dev = &pdev->dev; + syscon_rbm->reboot.write = syscon_reboot_mode_write; + syscon_rbm->mask = 0xffffffff; + + syscon_rbm->map = syscon_node_to_regmap(pdev->dev.parent->of_node); + if (IS_ERR(syscon_rbm->map)) + return PTR_ERR(syscon_rbm->map); + + if (of_property_read_u32(pdev->dev.of_node, "offset", + &syscon_rbm->offset)) + return -EINVAL; + + of_property_read_u32(pdev->dev.of_node, "mask", &syscon_rbm->mask); + + ret = devm_reboot_mode_register(&pdev->dev, &syscon_rbm->reboot); + if (ret) + dev_err(&pdev->dev, "can't register reboot mode\n"); + + return ret; +} + +static const struct of_device_id syscon_reboot_mode_of_match[] = { + { .compatible = "syscon-reboot-mode" }, + {} +}; +MODULE_DEVICE_TABLE(of, syscon_reboot_mode_of_match); + +static struct platform_driver syscon_reboot_mode_driver = { + .probe = syscon_reboot_mode_probe, + .driver = { + .name = "syscon-reboot-mode", + .of_match_table = syscon_reboot_mode_of_match, + }, +}; +module_platform_driver(syscon_reboot_mode_driver); + +MODULE_AUTHOR("Andy Yan <andy.yan@rock-chips.com"); +MODULE_DESCRIPTION("SYSCON reboot mode driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/reset/syscon-reboot.c b/drivers/power/reset/syscon-reboot.c new file mode 100644 index 000000000..510e36338 --- /dev/null +++ b/drivers/power/reset/syscon-reboot.c @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic Syscon Reboot Driver + * + * Copyright (c) 2013, Applied Micro Circuits Corporation + * Author: Feng Kan <fkan@apm.com> + */ +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/notifier.h> +#include <linux/mfd/syscon.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +struct syscon_reboot_context { + struct regmap *map; + u32 offset; + u32 value; + u32 mask; + struct notifier_block restart_handler; +}; + +static int syscon_restart_handle(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct syscon_reboot_context *ctx = + container_of(this, struct syscon_reboot_context, + restart_handler); + + /* Issue the reboot */ + regmap_update_bits(ctx->map, ctx->offset, ctx->mask, ctx->value); + + mdelay(1000); + + pr_emerg("Unable to restart system\n"); + return NOTIFY_DONE; +} + +static int syscon_reboot_probe(struct platform_device *pdev) +{ + struct syscon_reboot_context *ctx; + struct device *dev = &pdev->dev; + int mask_err, value_err; + int err; + + ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + ctx->map = syscon_regmap_lookup_by_phandle(dev->of_node, "regmap"); + if (IS_ERR(ctx->map)) { + ctx->map = syscon_node_to_regmap(dev->parent->of_node); + if (IS_ERR(ctx->map)) + return PTR_ERR(ctx->map); + } + + if (of_property_read_u32(pdev->dev.of_node, "offset", &ctx->offset)) + return -EINVAL; + + value_err = of_property_read_u32(pdev->dev.of_node, "value", &ctx->value); + mask_err = of_property_read_u32(pdev->dev.of_node, "mask", &ctx->mask); + if (value_err && mask_err) { + dev_err(dev, "unable to read 'value' and 'mask'"); + return -EINVAL; + } + + if (value_err) { + /* support old binding */ + ctx->value = ctx->mask; + ctx->mask = 0xFFFFFFFF; + } else if (mask_err) { + /* support value without mask*/ + ctx->mask = 0xFFFFFFFF; + } + + ctx->restart_handler.notifier_call = syscon_restart_handle; + ctx->restart_handler.priority = 192; + err = register_restart_handler(&ctx->restart_handler); + if (err) + dev_err(dev, "can't register restart notifier (err=%d)\n", err); + + return err; +} + +static const struct of_device_id syscon_reboot_of_match[] = { + { .compatible = "syscon-reboot" }, + {} +}; + +static struct platform_driver syscon_reboot_driver = { + .probe = syscon_reboot_probe, + .driver = { + .name = "syscon-reboot", + .of_match_table = syscon_reboot_of_match, + }, +}; +builtin_platform_driver(syscon_reboot_driver); diff --git a/drivers/power/reset/vexpress-poweroff.c b/drivers/power/reset/vexpress-poweroff.c new file mode 100644 index 000000000..1fdbcbd95 --- /dev/null +++ b/drivers/power/reset/vexpress-poweroff.c @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * + * Copyright (C) 2012 ARM Limited + */ + +#include <linux/delay.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/stat.h> +#include <linux/vexpress.h> + +static void vexpress_reset_do(struct device *dev, const char *what) +{ + int err = -ENOENT; + struct regmap *reg = dev_get_drvdata(dev); + + if (reg) { + err = regmap_write(reg, 0, 0); + if (!err) + mdelay(1000); + } + + dev_emerg(dev, "Unable to %s (%d)\n", what, err); +} + +static struct device *vexpress_power_off_device; +static atomic_t vexpress_restart_nb_refcnt = ATOMIC_INIT(0); + +static void vexpress_power_off(void) +{ + vexpress_reset_do(vexpress_power_off_device, "power off"); +} + +static struct device *vexpress_restart_device; + +static int vexpress_restart(struct notifier_block *this, unsigned long mode, + void *cmd) +{ + vexpress_reset_do(vexpress_restart_device, "restart"); + + return NOTIFY_DONE; +} + +static struct notifier_block vexpress_restart_nb = { + .notifier_call = vexpress_restart, + .priority = 128, +}; + +static ssize_t vexpress_reset_active_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sprintf(buf, "%d\n", vexpress_restart_device == dev); +} + +static ssize_t vexpress_reset_active_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + long value; + int err = kstrtol(buf, 0, &value); + + if (!err && value) + vexpress_restart_device = dev; + + return err ? err : count; +} + +static DEVICE_ATTR(active, S_IRUGO | S_IWUSR, vexpress_reset_active_show, + vexpress_reset_active_store); + + +enum vexpress_reset_func { FUNC_RESET, FUNC_SHUTDOWN, FUNC_REBOOT }; + +static const struct of_device_id vexpress_reset_of_match[] = { + { + .compatible = "arm,vexpress-reset", + .data = (void *)FUNC_RESET, + }, { + .compatible = "arm,vexpress-shutdown", + .data = (void *)FUNC_SHUTDOWN + }, { + .compatible = "arm,vexpress-reboot", + .data = (void *)FUNC_REBOOT + }, + {} +}; + +static int _vexpress_register_restart_handler(struct device *dev) +{ + int err; + + vexpress_restart_device = dev; + if (atomic_inc_return(&vexpress_restart_nb_refcnt) == 1) { + err = register_restart_handler(&vexpress_restart_nb); + if (err) { + dev_err(dev, "cannot register restart handler (err=%d)\n", err); + atomic_dec(&vexpress_restart_nb_refcnt); + return err; + } + } + device_create_file(dev, &dev_attr_active); + + return 0; +} + +static int vexpress_reset_probe(struct platform_device *pdev) +{ + const struct of_device_id *match = + of_match_device(vexpress_reset_of_match, &pdev->dev); + struct regmap *regmap; + int ret = 0; + + if (!match) + return -EINVAL; + + regmap = devm_regmap_init_vexpress_config(&pdev->dev); + if (IS_ERR(regmap)) + return PTR_ERR(regmap); + dev_set_drvdata(&pdev->dev, regmap); + + switch ((enum vexpress_reset_func)match->data) { + case FUNC_SHUTDOWN: + vexpress_power_off_device = &pdev->dev; + pm_power_off = vexpress_power_off; + break; + case FUNC_RESET: + if (!vexpress_restart_device) + ret = _vexpress_register_restart_handler(&pdev->dev); + break; + case FUNC_REBOOT: + ret = _vexpress_register_restart_handler(&pdev->dev); + break; + }; + + return ret; +} + +static struct platform_driver vexpress_reset_driver = { + .probe = vexpress_reset_probe, + .driver = { + .name = "vexpress-reset", + .of_match_table = vexpress_reset_of_match, + .suppress_bind_attrs = true, + }, +}; +builtin_platform_driver(vexpress_reset_driver); diff --git a/drivers/power/reset/xgene-reboot.c b/drivers/power/reset/xgene-reboot.c new file mode 100644 index 000000000..0b0d2fd2b --- /dev/null +++ b/drivers/power/reset/xgene-reboot.c @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AppliedMicro X-Gene SoC Reboot Driver + * + * Copyright (c) 2013, Applied Micro Circuits Corporation + * Author: Feng Kan <fkan@apm.com> + * Author: Loc Ho <lho@apm.com> + * + * This driver provides system reboot functionality for APM X-Gene SoC. + * For system shutdown, this is board specify. If a board designer + * implements GPIO shutdown, use the gpio-poweroff.c driver. + */ +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/notifier.h> +#include <linux/of_device.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/stat.h> +#include <linux/slab.h> + +struct xgene_reboot_context { + struct device *dev; + void *csr; + u32 mask; + struct notifier_block restart_handler; +}; + +static int xgene_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + struct xgene_reboot_context *ctx = + container_of(this, struct xgene_reboot_context, + restart_handler); + + /* Issue the reboot */ + writel(ctx->mask, ctx->csr); + + mdelay(1000); + + dev_emerg(ctx->dev, "Unable to restart system\n"); + + return NOTIFY_DONE; +} + +static int xgene_reboot_probe(struct platform_device *pdev) +{ + struct xgene_reboot_context *ctx; + struct device *dev = &pdev->dev; + int err; + + ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + ctx->csr = of_iomap(dev->of_node, 0); + if (!ctx->csr) { + dev_err(dev, "can not map resource\n"); + return -ENODEV; + } + + if (of_property_read_u32(dev->of_node, "mask", &ctx->mask)) + ctx->mask = 0xFFFFFFFF; + + ctx->dev = dev; + ctx->restart_handler.notifier_call = xgene_restart_handler; + ctx->restart_handler.priority = 128; + err = register_restart_handler(&ctx->restart_handler); + if (err) { + iounmap(ctx->csr); + dev_err(dev, "cannot register restart handler (err=%d)\n", err); + } + + return err; +} + +static const struct of_device_id xgene_reboot_of_match[] = { + { .compatible = "apm,xgene-reboot" }, + {} +}; + +static struct platform_driver xgene_reboot_driver = { + .probe = xgene_reboot_probe, + .driver = { + .name = "xgene-reboot", + .of_match_table = xgene_reboot_of_match, + }, +}; + +static int __init xgene_reboot_init(void) +{ + return platform_driver_register(&xgene_reboot_driver); +} +device_initcall(xgene_reboot_init); diff --git a/drivers/power/reset/zx-reboot.c b/drivers/power/reset/zx-reboot.c new file mode 100644 index 000000000..457950833 --- /dev/null +++ b/drivers/power/reset/zx-reboot.c @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ZTE zx296702 SoC reset code + * + * Copyright (c) 2015 Linaro Ltd. + * + * Author: Jun Nie <jun.nie@linaro.org> + */ + +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> + +static void __iomem *base; +static void __iomem *pcu_base; + +static int zx_restart_handler(struct notifier_block *this, + unsigned long mode, void *cmd) +{ + writel_relaxed(1, base + 0xb0); + writel_relaxed(1, pcu_base + 0x34); + + mdelay(50); + pr_emerg("Unable to restart system\n"); + + return NOTIFY_DONE; +} + +static struct notifier_block zx_restart_nb = { + .notifier_call = zx_restart_handler, + .priority = 128, +}; + +static int zx_reboot_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + int err; + + base = of_iomap(np, 0); + if (!base) { + WARN(1, "failed to map base address"); + return -ENODEV; + } + + np = of_find_compatible_node(NULL, NULL, "zte,zx296702-pcu"); + pcu_base = of_iomap(np, 0); + of_node_put(np); + if (!pcu_base) { + iounmap(base); + WARN(1, "failed to map pcu_base address"); + return -ENODEV; + } + + err = register_restart_handler(&zx_restart_nb); + if (err) { + iounmap(base); + iounmap(pcu_base); + dev_err(&pdev->dev, "Register restart handler failed(err=%d)\n", + err); + } + + return err; +} + +static const struct of_device_id zx_reboot_of_match[] = { + { .compatible = "zte,sysctrl" }, + {} +}; +MODULE_DEVICE_TABLE(of, zx_reboot_of_match); + +static struct platform_driver zx_reboot_driver = { + .probe = zx_reboot_probe, + .driver = { + .name = "zx-reboot", + .of_match_table = zx_reboot_of_match, + }, +}; +module_platform_driver(zx_reboot_driver); + +MODULE_DESCRIPTION("ZTE SoCs reset driver"); +MODULE_AUTHOR("Jun Nie <jun.nie@linaro.org>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/88pm860x_battery.c b/drivers/power/supply/88pm860x_battery.c new file mode 100644 index 000000000..590da88a1 --- /dev/null +++ b/drivers/power/supply/88pm860x_battery.c @@ -0,0 +1,1016 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for Marvell 88PM860x PMIC + * + * Copyright (c) 2012 Marvell International Ltd. + * Author: Jett Zhou <jtzhou@marvell.com> + * Haojian Zhuang <haojian.zhuang@marvell.com> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/mutex.h> +#include <linux/string.h> +#include <linux/power_supply.h> +#include <linux/mfd/88pm860x.h> +#include <linux/delay.h> + +/* bit definitions of Status Query Interface 2 */ +#define STATUS2_CHG (1 << 2) +#define STATUS2_BAT (1 << 3) +#define STATUS2_VBUS (1 << 4) + +/* bit definitions of Measurement Enable 1 Register */ +#define MEAS1_TINT (1 << 3) +#define MEAS1_GP1 (1 << 5) + +/* bit definitions of Measurement Enable 3 Register */ +#define MEAS3_IBAT (1 << 0) +#define MEAS3_BAT_DET (1 << 1) +#define MEAS3_CC (1 << 2) + +/* bit definitions of Measurement Off Time Register */ +#define MEAS_OFF_SLEEP_EN (1 << 1) + +/* bit definitions of GPADC Bias Current 2 Register */ +#define GPBIAS2_GPADC1_SET (2 << 4) +/* GPADC1 Bias Current value in uA unit */ +#define GPBIAS2_GPADC1_UA ((GPBIAS2_GPADC1_SET >> 4) * 5 + 1) + +/* bit definitions of GPADC Misc 1 Register */ +#define GPMISC1_GPADC_EN (1 << 0) + +/* bit definitions of Charger Control 6 Register */ +#define CC6_BAT_DET_GPADC1 1 + +/* bit definitions of Coulomb Counter Reading Register */ +#define CCNT_AVG_SEL (4 << 3) + +/* bit definitions of RTC miscellaneous Register1 */ +#define RTC_SOC_5LSB (0x1F << 3) + +/* bit definitions of RTC Register1 */ +#define RTC_SOC_3MSB (0x7) + +/* bit definitions of Power up Log register */ +#define BAT_WU_LOG (1<<6) + +/* coulomb counter index */ +#define CCNT_POS1 0 +#define CCNT_POS2 1 +#define CCNT_NEG1 2 +#define CCNT_NEG2 3 +#define CCNT_SPOS 4 +#define CCNT_SNEG 5 + +/* OCV -- Open Circuit Voltage */ +#define OCV_MODE_ACTIVE 0 +#define OCV_MODE_SLEEP 1 + +/* Vbat range of CC for measuring Rbat */ +#define LOW_BAT_THRESHOLD 3600 +#define VBATT_RESISTOR_MIN 3800 +#define VBATT_RESISTOR_MAX 4100 + +/* TBAT for batt, TINT for chip itself */ +#define PM860X_TEMP_TINT (0) +#define PM860X_TEMP_TBAT (1) + +/* + * Battery temperature based on NTC resistor, defined + * corresponding resistor value -- Ohm / C degeree. + */ +#define TBAT_NEG_25D 127773 /* -25 */ +#define TBAT_NEG_10D 54564 /* -10 */ +#define TBAT_0D 32330 /* 0 */ +#define TBAT_10D 19785 /* 10 */ +#define TBAT_20D 12468 /* 20 */ +#define TBAT_30D 8072 /* 30 */ +#define TBAT_40D 5356 /* 40 */ + +struct pm860x_battery_info { + struct pm860x_chip *chip; + struct i2c_client *i2c; + struct device *dev; + + struct power_supply *battery; + struct mutex lock; + int status; + int irq_cc; + int irq_batt; + int max_capacity; + int resistor; /* Battery Internal Resistor */ + int last_capacity; + int start_soc; + unsigned present:1; + unsigned temp_type:1; /* TINT or TBAT */ +}; + +struct ccnt { + unsigned long long int pos; + unsigned long long int neg; + unsigned int spos; + unsigned int sneg; + + int total_chg; /* mAh(3.6C) */ + int total_dischg; /* mAh(3.6C) */ +}; + +/* + * State of Charge. + * The first number is mAh(=3.6C), and the second number is percent point. + */ +static int array_soc[][2] = { + {4170, 100}, {4154, 99}, {4136, 98}, {4122, 97}, {4107, 96}, + {4102, 95}, {4088, 94}, {4081, 93}, {4070, 92}, {4060, 91}, + {4053, 90}, {4044, 89}, {4035, 88}, {4028, 87}, {4019, 86}, + {4013, 85}, {4006, 84}, {3995, 83}, {3987, 82}, {3982, 81}, + {3976, 80}, {3968, 79}, {3962, 78}, {3954, 77}, {3946, 76}, + {3941, 75}, {3934, 74}, {3929, 73}, {3922, 72}, {3916, 71}, + {3910, 70}, {3904, 69}, {3898, 68}, {3892, 67}, {3887, 66}, + {3880, 65}, {3874, 64}, {3868, 63}, {3862, 62}, {3854, 61}, + {3849, 60}, {3843, 59}, {3840, 58}, {3833, 57}, {3829, 56}, + {3824, 55}, {3818, 54}, {3815, 53}, {3810, 52}, {3808, 51}, + {3804, 50}, {3801, 49}, {3798, 48}, {3796, 47}, {3792, 46}, + {3789, 45}, {3785, 44}, {3784, 43}, {3782, 42}, {3780, 41}, + {3777, 40}, {3776, 39}, {3774, 38}, {3772, 37}, {3771, 36}, + {3769, 35}, {3768, 34}, {3764, 33}, {3763, 32}, {3760, 31}, + {3760, 30}, {3754, 29}, {3750, 28}, {3749, 27}, {3744, 26}, + {3740, 25}, {3734, 24}, {3732, 23}, {3728, 22}, {3726, 21}, + {3720, 20}, {3716, 19}, {3709, 18}, {3703, 17}, {3698, 16}, + {3692, 15}, {3683, 14}, {3675, 13}, {3670, 12}, {3665, 11}, + {3661, 10}, {3649, 9}, {3637, 8}, {3622, 7}, {3609, 6}, + {3580, 5}, {3558, 4}, {3540, 3}, {3510, 2}, {3429, 1}, +}; + +static struct ccnt ccnt_data; + +/* + * register 1 bit[7:0] -- bit[11:4] of measured value of voltage + * register 0 bit[3:0] -- bit[3:0] of measured value of voltage + */ +static int measure_12bit_voltage(struct pm860x_battery_info *info, + int offset, int *data) +{ + unsigned char buf[2]; + int ret; + + ret = pm860x_bulk_read(info->i2c, offset, 2, buf); + if (ret < 0) + return ret; + + *data = ((buf[0] & 0xff) << 4) | (buf[1] & 0x0f); + /* V_MEAS(mV) = data * 1.8 * 1000 / (2^12) */ + *data = ((*data & 0xfff) * 9 * 25) >> 9; + return 0; +} + +static int measure_vbatt(struct pm860x_battery_info *info, int state, + int *data) +{ + unsigned char buf[5]; + int ret; + + switch (state) { + case OCV_MODE_ACTIVE: + ret = measure_12bit_voltage(info, PM8607_VBAT_MEAS1, data); + if (ret) + return ret; + /* V_BATT_MEAS(mV) = value * 3 * 1.8 * 1000 / (2^12) */ + *data *= 3; + break; + case OCV_MODE_SLEEP: + /* + * voltage value of VBATT in sleep mode is saved in different + * registers. + * bit[11:10] -- bit[7:6] of LDO9(0x18) + * bit[9:8] -- bit[7:6] of LDO8(0x17) + * bit[7:6] -- bit[7:6] of LDO7(0x16) + * bit[5:4] -- bit[7:6] of LDO6(0x15) + * bit[3:0] -- bit[7:4] of LDO5(0x14) + */ + ret = pm860x_bulk_read(info->i2c, PM8607_LDO5, 5, buf); + if (ret < 0) + return ret; + ret = ((buf[4] >> 6) << 10) | ((buf[3] >> 6) << 8) + | ((buf[2] >> 6) << 6) | ((buf[1] >> 6) << 4) + | (buf[0] >> 4); + /* V_BATT_MEAS(mV) = data * 3 * 1.8 * 1000 / (2^12) */ + *data = ((*data & 0xff) * 27 * 25) >> 9; + break; + default: + return -EINVAL; + } + return 0; +} + +/* + * Return value is signed data. + * Negative value means discharging, and positive value means charging. + */ +static int measure_current(struct pm860x_battery_info *info, int *data) +{ + unsigned char buf[2]; + short s; + int ret; + + ret = pm860x_bulk_read(info->i2c, PM8607_IBAT_MEAS1, 2, buf); + if (ret < 0) + return ret; + + s = ((buf[0] & 0xff) << 8) | (buf[1] & 0xff); + /* current(mA) = value * 0.125 */ + *data = s >> 3; + return 0; +} + +static int set_charger_current(struct pm860x_battery_info *info, int data, + int *old) +{ + int ret; + + if (data < 50 || data > 1600 || !old) + return -EINVAL; + + data = ((data - 50) / 50) & 0x1f; + *old = pm860x_reg_read(info->i2c, PM8607_CHG_CTRL2); + *old = (*old & 0x1f) * 50 + 50; + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL2, 0x1f, data); + if (ret < 0) + return ret; + return 0; +} + +static int read_ccnt(struct pm860x_battery_info *info, int offset, + int *ccnt) +{ + unsigned char buf[2]; + int ret; + + ret = pm860x_set_bits(info->i2c, PM8607_CCNT, 7, offset & 7); + if (ret < 0) + goto out; + ret = pm860x_bulk_read(info->i2c, PM8607_CCNT_MEAS1, 2, buf); + if (ret < 0) + goto out; + *ccnt = ((buf[0] & 0xff) << 8) | (buf[1] & 0xff); + return 0; +out: + return ret; +} + +static int calc_ccnt(struct pm860x_battery_info *info, struct ccnt *ccnt) +{ + unsigned int sum; + int ret; + int data; + + ret = read_ccnt(info, CCNT_POS1, &data); + if (ret) + goto out; + sum = data & 0xffff; + ret = read_ccnt(info, CCNT_POS2, &data); + if (ret) + goto out; + sum |= (data & 0xffff) << 16; + ccnt->pos += sum; + + ret = read_ccnt(info, CCNT_NEG1, &data); + if (ret) + goto out; + sum = data & 0xffff; + ret = read_ccnt(info, CCNT_NEG2, &data); + if (ret) + goto out; + sum |= (data & 0xffff) << 16; + sum = ~sum + 1; /* since it's negative */ + ccnt->neg += sum; + + ret = read_ccnt(info, CCNT_SPOS, &data); + if (ret) + goto out; + ccnt->spos += data; + ret = read_ccnt(info, CCNT_SNEG, &data); + if (ret) + goto out; + + /* + * charge(mAh) = count * 1.6984 * 1e(-8) + * = count * 16984 * 1.024 * 1.024 * 1.024 / (2 ^ 40) + * = count * 18236 / (2 ^ 40) + */ + ccnt->total_chg = (int) ((ccnt->pos * 18236) >> 40); + ccnt->total_dischg = (int) ((ccnt->neg * 18236) >> 40); + return 0; +out: + return ret; +} + +static int clear_ccnt(struct pm860x_battery_info *info, struct ccnt *ccnt) +{ + int data; + + memset(ccnt, 0, sizeof(*ccnt)); + /* read to clear ccnt */ + read_ccnt(info, CCNT_POS1, &data); + read_ccnt(info, CCNT_POS2, &data); + read_ccnt(info, CCNT_NEG1, &data); + read_ccnt(info, CCNT_NEG2, &data); + read_ccnt(info, CCNT_SPOS, &data); + read_ccnt(info, CCNT_SNEG, &data); + return 0; +} + +/* Calculate Open Circuit Voltage */ +static int calc_ocv(struct pm860x_battery_info *info, int *ocv) +{ + int ret; + int i; + int data; + int vbatt_avg; + int vbatt_sum; + int ibatt_avg; + int ibatt_sum; + + if (!ocv) + return -EINVAL; + + for (i = 0, ibatt_sum = 0, vbatt_sum = 0; i < 10; i++) { + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + goto out; + vbatt_sum += data; + ret = measure_current(info, &data); + if (ret) + goto out; + ibatt_sum += data; + } + vbatt_avg = vbatt_sum / 10; + ibatt_avg = ibatt_sum / 10; + + mutex_lock(&info->lock); + if (info->present) + *ocv = vbatt_avg - ibatt_avg * info->resistor / 1000; + else + *ocv = vbatt_avg; + mutex_unlock(&info->lock); + dev_dbg(info->dev, "VBAT average:%d, OCV:%d\n", vbatt_avg, *ocv); + return 0; +out: + return ret; +} + +/* Calculate State of Charge (percent points) */ +static int calc_soc(struct pm860x_battery_info *info, int state, int *soc) +{ + int i; + int ocv; + int count; + int ret = -EINVAL; + + if (!soc) + return -EINVAL; + + switch (state) { + case OCV_MODE_ACTIVE: + ret = calc_ocv(info, &ocv); + break; + case OCV_MODE_SLEEP: + ret = measure_vbatt(info, OCV_MODE_SLEEP, &ocv); + break; + } + if (ret) + return ret; + + count = ARRAY_SIZE(array_soc); + if (ocv < array_soc[count - 1][0]) { + *soc = 0; + return 0; + } + + for (i = 0; i < count; i++) { + if (ocv >= array_soc[i][0]) { + *soc = array_soc[i][1]; + break; + } + } + return 0; +} + +static irqreturn_t pm860x_coulomb_handler(int irq, void *data) +{ + struct pm860x_battery_info *info = data; + + calc_ccnt(info, &ccnt_data); + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_batt_handler(int irq, void *data) +{ + struct pm860x_battery_info *info = data; + int ret; + + mutex_lock(&info->lock); + ret = pm860x_reg_read(info->i2c, PM8607_STATUS_2); + if (ret & STATUS2_BAT) { + info->present = 1; + info->temp_type = PM860X_TEMP_TBAT; + } else { + info->present = 0; + info->temp_type = PM860X_TEMP_TINT; + } + mutex_unlock(&info->lock); + /* clear ccnt since battery is attached or dettached */ + clear_ccnt(info, &ccnt_data); + return IRQ_HANDLED; +} + +static void pm860x_init_battery(struct pm860x_battery_info *info) +{ + unsigned char buf[2]; + int ret; + int data; + int bat_remove; + int soc = 0; + + /* measure enable on GPADC1 */ + data = MEAS1_GP1; + if (info->temp_type == PM860X_TEMP_TINT) + data |= MEAS1_TINT; + ret = pm860x_set_bits(info->i2c, PM8607_MEAS_EN1, data, data); + if (ret) + goto out; + + /* measure enable on IBAT, BAT_DET, CC. IBAT is depend on CC. */ + data = MEAS3_IBAT | MEAS3_BAT_DET | MEAS3_CC; + ret = pm860x_set_bits(info->i2c, PM8607_MEAS_EN3, data, data); + if (ret) + goto out; + + /* measure disable CC in sleep time */ + ret = pm860x_reg_write(info->i2c, PM8607_MEAS_OFF_TIME1, 0x82); + if (ret) + goto out; + ret = pm860x_reg_write(info->i2c, PM8607_MEAS_OFF_TIME2, 0x6c); + if (ret) + goto out; + + /* enable GPADC */ + ret = pm860x_set_bits(info->i2c, PM8607_GPADC_MISC1, + GPMISC1_GPADC_EN, GPMISC1_GPADC_EN); + if (ret < 0) + goto out; + + /* detect battery via GPADC1 */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL6, + CC6_BAT_DET_GPADC1, CC6_BAT_DET_GPADC1); + if (ret < 0) + goto out; + + ret = pm860x_set_bits(info->i2c, PM8607_CCNT, 7 << 3, + CCNT_AVG_SEL); + if (ret < 0) + goto out; + + /* set GPADC1 bias */ + ret = pm860x_set_bits(info->i2c, PM8607_GP_BIAS2, 0xF << 4, + GPBIAS2_GPADC1_SET); + if (ret < 0) + goto out; + + /* check whether battery present) */ + mutex_lock(&info->lock); + ret = pm860x_reg_read(info->i2c, PM8607_STATUS_2); + if (ret < 0) { + mutex_unlock(&info->lock); + goto out; + } + if (ret & STATUS2_BAT) { + info->present = 1; + info->temp_type = PM860X_TEMP_TBAT; + } else { + info->present = 0; + info->temp_type = PM860X_TEMP_TINT; + } + mutex_unlock(&info->lock); + + ret = calc_soc(info, OCV_MODE_ACTIVE, &soc); + if (ret < 0) + goto out; + + data = pm860x_reg_read(info->i2c, PM8607_POWER_UP_LOG); + bat_remove = data & BAT_WU_LOG; + + dev_dbg(info->dev, "battery wake up? %s\n", + bat_remove != 0 ? "yes" : "no"); + + /* restore SOC from RTC domain register */ + if (bat_remove == 0) { + buf[0] = pm860x_reg_read(info->i2c, PM8607_RTC_MISC2); + buf[1] = pm860x_reg_read(info->i2c, PM8607_RTC1); + data = ((buf[1] & 0x3) << 5) | ((buf[0] >> 3) & 0x1F); + if (data > soc + 15) + info->start_soc = soc; + else if (data < soc - 15) + info->start_soc = soc; + else + info->start_soc = data; + dev_dbg(info->dev, "soc_rtc %d, soc_ocv :%d\n", data, soc); + } else { + pm860x_set_bits(info->i2c, PM8607_POWER_UP_LOG, + BAT_WU_LOG, BAT_WU_LOG); + info->start_soc = soc; + } + info->last_capacity = info->start_soc; + dev_dbg(info->dev, "init soc : %d\n", info->last_capacity); +out: + return; +} + +static void set_temp_threshold(struct pm860x_battery_info *info, + int min, int max) +{ + int data; + + /* (tmp << 8) / 1800 */ + if (min <= 0) + data = 0; + else + data = (min << 8) / 1800; + pm860x_reg_write(info->i2c, PM8607_GPADC1_HIGHTH, data); + dev_dbg(info->dev, "TEMP_HIGHTH : min: %d, 0x%x\n", min, data); + + if (max <= 0) + data = 0xff; + else + data = (max << 8) / 1800; + pm860x_reg_write(info->i2c, PM8607_GPADC1_LOWTH, data); + dev_dbg(info->dev, "TEMP_LOWTH:max : %d, 0x%x\n", max, data); +} + +static int measure_temp(struct pm860x_battery_info *info, int *data) +{ + int ret; + int temp; + int min; + int max; + + if (info->temp_type == PM860X_TEMP_TINT) { + ret = measure_12bit_voltage(info, PM8607_TINT_MEAS1, data); + if (ret) + return ret; + *data = (*data - 884) * 1000 / 3611; + } else { + ret = measure_12bit_voltage(info, PM8607_GPADC1_MEAS1, data); + if (ret) + return ret; + /* meausered Vtbat(mV) / Ibias_current(11uA)*/ + *data = (*data * 1000) / GPBIAS2_GPADC1_UA; + + if (*data > TBAT_NEG_25D) { + temp = -30; /* over cold , suppose -30 roughly */ + max = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, 0, max); + } else if (*data > TBAT_NEG_10D) { + temp = -15; /* -15 degree, code */ + max = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, 0, max); + } else if (*data > TBAT_0D) { + temp = -5; /* -5 degree */ + min = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + max = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, max); + } else if (*data > TBAT_10D) { + temp = 5; /* in range of (0, 10) */ + min = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + max = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, max); + } else if (*data > TBAT_20D) { + temp = 15; /* in range of (10, 20) */ + min = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + max = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, max); + } else if (*data > TBAT_30D) { + temp = 25; /* in range of (20, 30) */ + min = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + max = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, max); + } else if (*data > TBAT_40D) { + temp = 35; /* in range of (30, 40) */ + min = TBAT_NEG_10D * GPBIAS2_GPADC1_UA / 1000; + max = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, max); + } else { + min = TBAT_40D * GPBIAS2_GPADC1_UA / 1000; + set_temp_threshold(info, min, 0); + temp = 45; /* over heat ,suppose 45 roughly */ + } + + dev_dbg(info->dev, "temp_C:%d C,temp_mv:%d mv\n", temp, *data); + *data = temp; + } + return 0; +} + +static int calc_resistor(struct pm860x_battery_info *info) +{ + int vbatt_sum1; + int vbatt_sum2; + int chg_current; + int ibatt_sum1; + int ibatt_sum2; + int data; + int ret; + int i; + + ret = measure_current(info, &data); + /* make sure that charging is launched by data > 0 */ + if (ret || data < 0) + goto out; + + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + goto out; + /* calculate resistor only in CC charge mode */ + if (data < VBATT_RESISTOR_MIN || data > VBATT_RESISTOR_MAX) + goto out; + + /* current is saved */ + if (set_charger_current(info, 500, &chg_current)) + goto out; + + /* + * set charge current as 500mA, wait about 500ms till charging + * process is launched and stable with the newer charging current. + */ + msleep(500); + + for (i = 0, vbatt_sum1 = 0, ibatt_sum1 = 0; i < 10; i++) { + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + goto out_meas; + vbatt_sum1 += data; + ret = measure_current(info, &data); + if (ret) + goto out_meas; + + if (data < 0) + ibatt_sum1 = ibatt_sum1 - data; /* discharging */ + else + ibatt_sum1 = ibatt_sum1 + data; /* charging */ + } + + if (set_charger_current(info, 100, &ret)) + goto out_meas; + /* + * set charge current as 100mA, wait about 500ms till charging + * process is launched and stable with the newer charging current. + */ + msleep(500); + + for (i = 0, vbatt_sum2 = 0, ibatt_sum2 = 0; i < 10; i++) { + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + goto out_meas; + vbatt_sum2 += data; + ret = measure_current(info, &data); + if (ret) + goto out_meas; + + if (data < 0) + ibatt_sum2 = ibatt_sum2 - data; /* discharging */ + else + ibatt_sum2 = ibatt_sum2 + data; /* charging */ + } + + /* restore current setting */ + if (set_charger_current(info, chg_current, &ret)) + goto out_meas; + + if ((vbatt_sum1 > vbatt_sum2) && (ibatt_sum1 > ibatt_sum2) && + (ibatt_sum2 > 0)) { + /* calculate resistor in discharging case */ + data = 1000 * (vbatt_sum1 - vbatt_sum2) + / (ibatt_sum1 - ibatt_sum2); + if ((data - info->resistor > 0) && + (data - info->resistor < info->resistor)) + info->resistor = data; + if ((info->resistor - data > 0) && + (info->resistor - data < data)) + info->resistor = data; + } + return 0; + +out_meas: + set_charger_current(info, chg_current, &ret); +out: + return -EINVAL; +} + +static int calc_capacity(struct pm860x_battery_info *info, int *cap) +{ + int ret; + int data; + int ibat; + int cap_ocv = 0; + int cap_cc = 0; + + ret = calc_ccnt(info, &ccnt_data); + if (ret) + goto out; +soc: + data = info->max_capacity * info->start_soc / 100; + if (ccnt_data.total_dischg - ccnt_data.total_chg <= data) { + cap_cc = + data + ccnt_data.total_chg - ccnt_data.total_dischg; + } else { + clear_ccnt(info, &ccnt_data); + calc_soc(info, OCV_MODE_ACTIVE, &info->start_soc); + dev_dbg(info->dev, "restart soc = %d !\n", + info->start_soc); + goto soc; + } + + cap_cc = cap_cc * 100 / info->max_capacity; + if (cap_cc < 0) + cap_cc = 0; + else if (cap_cc > 100) + cap_cc = 100; + + dev_dbg(info->dev, "%s, last cap : %d", __func__, + info->last_capacity); + + ret = measure_current(info, &ibat); + if (ret) + goto out; + /* Calculate the capacity when discharging(ibat < 0) */ + if (ibat < 0) { + ret = calc_soc(info, OCV_MODE_ACTIVE, &cap_ocv); + if (ret) + cap_ocv = info->last_capacity; + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + goto out; + if (data <= LOW_BAT_THRESHOLD) { + /* choose the lower capacity value to report + * between vbat and CC when vbat < 3.6v; + * than 3.6v; + */ + *cap = min(cap_ocv, cap_cc); + } else { + /* when detect vbat > 3.6v, but cap_cc < 15,and + * cap_ocv is 10% larger than cap_cc, we can think + * CC have some accumulation error, switch to OCV + * to estimate capacity; + * */ + if (cap_cc < 15 && cap_ocv - cap_cc > 10) + *cap = cap_ocv; + else + *cap = cap_cc; + } + /* when discharging, make sure current capacity + * is lower than last*/ + if (*cap > info->last_capacity) + *cap = info->last_capacity; + } else { + *cap = cap_cc; + } + info->last_capacity = *cap; + + dev_dbg(info->dev, "%s, cap_ocv:%d cap_cc:%d, cap:%d\n", + (ibat < 0) ? "discharging" : "charging", + cap_ocv, cap_cc, *cap); + /* + * store the current capacity to RTC domain register, + * after next power up , it will be restored. + */ + pm860x_set_bits(info->i2c, PM8607_RTC_MISC2, RTC_SOC_5LSB, + (*cap & 0x1F) << 3); + pm860x_set_bits(info->i2c, PM8607_RTC1, RTC_SOC_3MSB, + ((*cap >> 5) & 0x3)); + return 0; +out: + return ret; +} + +static void pm860x_external_power_changed(struct power_supply *psy) +{ + struct pm860x_battery_info *info = dev_get_drvdata(psy->dev.parent); + + calc_resistor(info); +} + +static int pm860x_batt_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pm860x_battery_info *info = dev_get_drvdata(psy->dev.parent); + int data; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + val->intval = info->present; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = calc_capacity(info, &data); + if (ret) + return ret; + if (data < 0) + data = 0; + else if (data > 100) + data = 100; + /* return 100 if battery is not attached */ + if (!info->present) + data = 100; + val->intval = data; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + /* return real vbatt Voltage */ + ret = measure_vbatt(info, OCV_MODE_ACTIVE, &data); + if (ret) + return ret; + val->intval = data * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + /* return Open Circuit Voltage (not measured voltage) */ + ret = calc_ocv(info, &data); + if (ret) + return ret; + val->intval = data * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = measure_current(info, &data); + if (ret) + return ret; + val->intval = data; + break; + case POWER_SUPPLY_PROP_TEMP: + if (info->present) { + ret = measure_temp(info, &data); + if (ret) + return ret; + data *= 10; + } else { + /* Fake Temp 25C Without Battery */ + data = 250; + } + val->intval = data; + break; + default: + return -ENODEV; + } + return 0; +} + +static int pm860x_batt_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct pm860x_battery_info *info = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + clear_ccnt(info, &ccnt_data); + info->start_soc = 100; + dev_dbg(info->dev, "chg done, update soc = %d\n", + info->start_soc); + break; + default: + return -EPERM; + } + + return 0; +} + + +static enum power_supply_property pm860x_batt_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_TEMP, +}; + +static const struct power_supply_desc pm860x_battery_desc = { + .name = "battery-monitor", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = pm860x_batt_props, + .num_properties = ARRAY_SIZE(pm860x_batt_props), + .get_property = pm860x_batt_get_prop, + .set_property = pm860x_batt_set_prop, + .external_power_changed = pm860x_external_power_changed, +}; + +static int pm860x_battery_probe(struct platform_device *pdev) +{ + struct pm860x_chip *chip = dev_get_drvdata(pdev->dev.parent); + struct pm860x_battery_info *info; + struct pm860x_power_pdata *pdata; + int ret; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->irq_cc = platform_get_irq(pdev, 0); + if (info->irq_cc <= 0) + return -EINVAL; + + info->irq_batt = platform_get_irq(pdev, 1); + if (info->irq_batt <= 0) + return -EINVAL; + + info->chip = chip; + info->i2c = + (chip->id == CHIP_PM8607) ? chip->client : chip->companion; + info->dev = &pdev->dev; + info->status = POWER_SUPPLY_STATUS_UNKNOWN; + pdata = pdev->dev.platform_data; + + mutex_init(&info->lock); + platform_set_drvdata(pdev, info); + + pm860x_init_battery(info); + + if (pdata && pdata->max_capacity) + info->max_capacity = pdata->max_capacity; + else + info->max_capacity = 1500; /* set default capacity */ + if (pdata && pdata->resistor) + info->resistor = pdata->resistor; + else + info->resistor = 300; /* set default internal resistor */ + + info->battery = devm_power_supply_register(&pdev->dev, + &pm860x_battery_desc, + NULL); + if (IS_ERR(info->battery)) + return PTR_ERR(info->battery); + info->battery->dev.parent = &pdev->dev; + + ret = devm_request_threaded_irq(chip->dev, info->irq_cc, NULL, + pm860x_coulomb_handler, IRQF_ONESHOT, + "coulomb", info); + if (ret < 0) { + dev_err(chip->dev, "Failed to request IRQ: #%d: %d\n", + info->irq_cc, ret); + return ret; + } + + ret = devm_request_threaded_irq(chip->dev, info->irq_batt, NULL, + pm860x_batt_handler, + IRQF_ONESHOT, "battery", info); + if (ret < 0) { + dev_err(chip->dev, "Failed to request IRQ: #%d: %d\n", + info->irq_batt, ret); + return ret; + } + + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int pm860x_battery_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct pm860x_chip *chip = dev_get_drvdata(pdev->dev.parent); + + if (device_may_wakeup(dev)) + chip->wakeup_flag |= 1 << PM8607_IRQ_CC; + return 0; +} + +static int pm860x_battery_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct pm860x_chip *chip = dev_get_drvdata(pdev->dev.parent); + + if (device_may_wakeup(dev)) + chip->wakeup_flag &= ~(1 << PM8607_IRQ_CC); + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(pm860x_battery_pm_ops, + pm860x_battery_suspend, pm860x_battery_resume); + +static struct platform_driver pm860x_battery_driver = { + .driver = { + .name = "88pm860x-battery", + .pm = &pm860x_battery_pm_ops, + }, + .probe = pm860x_battery_probe, +}; +module_platform_driver(pm860x_battery_driver); + +MODULE_DESCRIPTION("Marvell 88PM860x Battery driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/88pm860x_charger.c b/drivers/power/supply/88pm860x_charger.c new file mode 100644 index 000000000..f21ce52fb --- /dev/null +++ b/drivers/power/supply/88pm860x_charger.c @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for Marvell 88PM860x PMIC + * + * Copyright (c) 2012 Marvell International Ltd. + * Author: Jett Zhou <jtzhou@marvell.com> + * Haojian Zhuang <haojian.zhuang@marvell.com> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/power_supply.h> +#include <linux/mfd/88pm860x.h> +#include <linux/delay.h> +#include <linux/uaccess.h> +#include <asm/div64.h> + +/* bit definitions of Status Query Interface 2 */ +#define STATUS2_CHG (1 << 2) + +/* bit definitions of Reset Out Register */ +#define RESET_SW_PD (1 << 7) + +/* bit definitions of PreReg 1 */ +#define PREREG1_90MA (0x0) +#define PREREG1_180MA (0x1) +#define PREREG1_450MA (0x4) +#define PREREG1_540MA (0x5) +#define PREREG1_1350MA (0xE) +#define PREREG1_VSYS_4_5V (3 << 4) + +/* bit definitions of Charger Control 1 Register */ +#define CC1_MODE_OFF (0) +#define CC1_MODE_PRECHARGE (1) +#define CC1_MODE_FASTCHARGE (2) +#define CC1_MODE_PULSECHARGE (3) +#define CC1_ITERM_20MA (0 << 2) +#define CC1_ITERM_60MA (2 << 2) +#define CC1_VFCHG_4_2V (9 << 4) + +/* bit definitions of Charger Control 2 Register */ +#define CC2_ICHG_100MA (0x1) +#define CC2_ICHG_500MA (0x9) +#define CC2_ICHG_1000MA (0x13) + +/* bit definitions of Charger Control 3 Register */ +#define CC3_180MIN_TIMEOUT (0x6 << 4) +#define CC3_270MIN_TIMEOUT (0x7 << 4) +#define CC3_360MIN_TIMEOUT (0xA << 4) +#define CC3_DISABLE_TIMEOUT (0xF << 4) + +/* bit definitions of Charger Control 4 Register */ +#define CC4_IPRE_40MA (7) +#define CC4_VPCHG_3_2V (3 << 4) +#define CC4_IFCHG_MON_EN (1 << 6) +#define CC4_BTEMP_MON_EN (1 << 7) + +/* bit definitions of Charger Control 6 Register */ +#define CC6_BAT_OV_EN (1 << 2) +#define CC6_BAT_UV_EN (1 << 3) +#define CC6_UV_VBAT_SET (0x3 << 6) /* 2.8v */ + +/* bit definitions of Charger Control 7 Register */ +#define CC7_BAT_REM_EN (1 << 3) +#define CC7_IFSM_EN (1 << 7) + +/* bit definitions of Measurement Enable 1 Register */ +#define MEAS1_VBAT (1 << 0) + +/* bit definitions of Measurement Enable 3 Register */ +#define MEAS3_IBAT_EN (1 << 0) +#define MEAS3_CC_EN (1 << 2) + +#define FSM_INIT 0 +#define FSM_DISCHARGE 1 +#define FSM_PRECHARGE 2 +#define FSM_FASTCHARGE 3 + +#define PRECHARGE_THRESHOLD 3100 +#define POWEROFF_THRESHOLD 3400 +#define CHARGE_THRESHOLD 4000 +#define DISCHARGE_THRESHOLD 4180 + +/* over-temperature on PM8606 setting */ +#define OVER_TEMP_FLAG (1 << 6) +#define OVTEMP_AUTORECOVER (1 << 3) + +/* over-voltage protect on vchg setting mv */ +#define VCHG_NORMAL_LOW 4200 +#define VCHG_NORMAL_CHECK 5800 +#define VCHG_NORMAL_HIGH 6000 +#define VCHG_OVP_LOW 5500 + +struct pm860x_charger_info { + struct pm860x_chip *chip; + struct i2c_client *i2c; + struct i2c_client *i2c_8606; + struct device *dev; + + struct power_supply *usb; + struct mutex lock; + int irq_nums; + int irq[7]; + unsigned state:3; /* fsm state */ + unsigned online:1; /* usb charger */ + unsigned present:1; /* battery present */ + unsigned allowed:1; +}; + +static char *pm860x_supplied_to[] = { + "battery-monitor", +}; + +static int measure_vchg(struct pm860x_charger_info *info, int *data) +{ + unsigned char buf[2]; + int ret = 0; + + ret = pm860x_bulk_read(info->i2c, PM8607_VCHG_MEAS1, 2, buf); + if (ret < 0) + return ret; + + *data = ((buf[0] & 0xff) << 4) | (buf[1] & 0x0f); + /* V_BATT_MEAS(mV) = value * 5 * 1.8 * 1000 / (2^12) */ + *data = ((*data & 0xfff) * 9 * 125) >> 9; + + dev_dbg(info->dev, "%s, vchg: %d mv\n", __func__, *data); + + return ret; +} + +static void set_vchg_threshold(struct pm860x_charger_info *info, + int min, int max) +{ + int data; + + /* (tmp << 8) * / 5 / 1800 */ + if (min <= 0) + data = 0; + else + data = (min << 5) / 1125; + pm860x_reg_write(info->i2c, PM8607_VCHG_LOWTH, data); + dev_dbg(info->dev, "VCHG_LOWTH:%dmv, 0x%x\n", min, data); + + if (max <= 0) + data = 0xff; + else + data = (max << 5) / 1125; + pm860x_reg_write(info->i2c, PM8607_VCHG_HIGHTH, data); + dev_dbg(info->dev, "VCHG_HIGHTH:%dmv, 0x%x\n", max, data); + +} + +static void set_vbatt_threshold(struct pm860x_charger_info *info, + int min, int max) +{ + int data; + + /* (tmp << 8) * 3 / 1800 */ + if (min <= 0) + data = 0; + else + data = (min << 5) / 675; + pm860x_reg_write(info->i2c, PM8607_VBAT_LOWTH, data); + dev_dbg(info->dev, "VBAT Min:%dmv, LOWTH:0x%x\n", min, data); + + if (max <= 0) + data = 0xff; + else + data = (max << 5) / 675; + pm860x_reg_write(info->i2c, PM8607_VBAT_HIGHTH, data); + dev_dbg(info->dev, "VBAT Max:%dmv, HIGHTH:0x%x\n", max, data); + + return; +} + +static int start_precharge(struct pm860x_charger_info *info) +{ + int ret; + + dev_dbg(info->dev, "Start Pre-charging!\n"); + set_vbatt_threshold(info, 0, 0); + + ret = pm860x_reg_write(info->i2c_8606, PM8606_PREREGULATORA, + PREREG1_1350MA | PREREG1_VSYS_4_5V); + if (ret < 0) + goto out; + /* stop charging */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL1, 3, + CC1_MODE_OFF); + if (ret < 0) + goto out; + /* set 270 minutes timeout */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL3, (0xf << 4), + CC3_270MIN_TIMEOUT); + if (ret < 0) + goto out; + /* set precharge current, termination voltage, IBAT & TBAT monitor */ + ret = pm860x_reg_write(info->i2c, PM8607_CHG_CTRL4, + CC4_IPRE_40MA | CC4_VPCHG_3_2V | + CC4_IFCHG_MON_EN | CC4_BTEMP_MON_EN); + if (ret < 0) + goto out; + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL7, + CC7_BAT_REM_EN | CC7_IFSM_EN, + CC7_BAT_REM_EN | CC7_IFSM_EN); + if (ret < 0) + goto out; + /* trigger precharge */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL1, 3, + CC1_MODE_PRECHARGE); +out: + return ret; +} + +static int start_fastcharge(struct pm860x_charger_info *info) +{ + int ret; + + dev_dbg(info->dev, "Start Fast-charging!\n"); + + /* set fastcharge termination current & voltage, disable charging */ + ret = pm860x_reg_write(info->i2c, PM8607_CHG_CTRL1, + CC1_MODE_OFF | CC1_ITERM_60MA | + CC1_VFCHG_4_2V); + if (ret < 0) + goto out; + ret = pm860x_reg_write(info->i2c_8606, PM8606_PREREGULATORA, + PREREG1_540MA | PREREG1_VSYS_4_5V); + if (ret < 0) + goto out; + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL2, 0x1f, + CC2_ICHG_500MA); + if (ret < 0) + goto out; + /* set 270 minutes timeout */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL3, (0xf << 4), + CC3_270MIN_TIMEOUT); + if (ret < 0) + goto out; + /* set IBAT & TBAT monitor */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL4, + CC4_IFCHG_MON_EN | CC4_BTEMP_MON_EN, + CC4_IFCHG_MON_EN | CC4_BTEMP_MON_EN); + if (ret < 0) + goto out; + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL6, + CC6_BAT_OV_EN | CC6_BAT_UV_EN | + CC6_UV_VBAT_SET, + CC6_BAT_OV_EN | CC6_BAT_UV_EN | + CC6_UV_VBAT_SET); + if (ret < 0) + goto out; + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL7, + CC7_BAT_REM_EN | CC7_IFSM_EN, + CC7_BAT_REM_EN | CC7_IFSM_EN); + if (ret < 0) + goto out; + /* launch fast-charge */ + ret = pm860x_set_bits(info->i2c, PM8607_CHG_CTRL1, 3, + CC1_MODE_FASTCHARGE); + /* vchg threshold setting */ + set_vchg_threshold(info, VCHG_NORMAL_LOW, VCHG_NORMAL_HIGH); +out: + return ret; +} + +static void stop_charge(struct pm860x_charger_info *info, int vbatt) +{ + dev_dbg(info->dev, "Stop charging!\n"); + pm860x_set_bits(info->i2c, PM8607_CHG_CTRL1, 3, CC1_MODE_OFF); + if (vbatt > CHARGE_THRESHOLD && info->online) + set_vbatt_threshold(info, CHARGE_THRESHOLD, 0); +} + +static void power_off_notification(struct pm860x_charger_info *info) +{ + dev_dbg(info->dev, "Power-off notification!\n"); +} + +static int set_charging_fsm(struct pm860x_charger_info *info) +{ + struct power_supply *psy; + union power_supply_propval data; + unsigned char fsm_state[][16] = { "init", "discharge", "precharge", + "fastcharge", + }; + int ret; + int vbatt; + + psy = power_supply_get_by_name(pm860x_supplied_to[0]); + if (!psy) + return -EINVAL; + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_VOLTAGE_NOW, + &data); + if (ret) { + power_supply_put(psy); + return ret; + } + vbatt = data.intval / 1000; + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_PRESENT, &data); + if (ret) { + power_supply_put(psy); + return ret; + } + power_supply_put(psy); + + mutex_lock(&info->lock); + info->present = data.intval; + + dev_dbg(info->dev, "Entering FSM:%s, Charger:%s, Battery:%s, " + "Allowed:%d\n", + &fsm_state[info->state][0], + (info->online) ? "online" : "N/A", + (info->present) ? "present" : "N/A", info->allowed); + dev_dbg(info->dev, "set_charging_fsm:vbatt:%d(mV)\n", vbatt); + + switch (info->state) { + case FSM_INIT: + if (info->online && info->present && info->allowed) { + if (vbatt < PRECHARGE_THRESHOLD) { + info->state = FSM_PRECHARGE; + start_precharge(info); + } else if (vbatt > DISCHARGE_THRESHOLD) { + info->state = FSM_DISCHARGE; + stop_charge(info, vbatt); + } else if (vbatt < DISCHARGE_THRESHOLD) { + info->state = FSM_FASTCHARGE; + start_fastcharge(info); + } + } else { + if (vbatt < POWEROFF_THRESHOLD) { + power_off_notification(info); + } else { + info->state = FSM_DISCHARGE; + stop_charge(info, vbatt); + } + } + break; + case FSM_PRECHARGE: + if (info->online && info->present && info->allowed) { + if (vbatt > PRECHARGE_THRESHOLD) { + info->state = FSM_FASTCHARGE; + start_fastcharge(info); + } + } else { + info->state = FSM_DISCHARGE; + stop_charge(info, vbatt); + } + break; + case FSM_FASTCHARGE: + if (info->online && info->present && info->allowed) { + if (vbatt < PRECHARGE_THRESHOLD) { + info->state = FSM_PRECHARGE; + start_precharge(info); + } + } else { + info->state = FSM_DISCHARGE; + stop_charge(info, vbatt); + } + break; + case FSM_DISCHARGE: + if (info->online && info->present && info->allowed) { + if (vbatt < PRECHARGE_THRESHOLD) { + info->state = FSM_PRECHARGE; + start_precharge(info); + } else if (vbatt < DISCHARGE_THRESHOLD) { + info->state = FSM_FASTCHARGE; + start_fastcharge(info); + } + } else { + if (vbatt < POWEROFF_THRESHOLD) + power_off_notification(info); + else if (vbatt > CHARGE_THRESHOLD && info->online) + set_vbatt_threshold(info, CHARGE_THRESHOLD, 0); + } + break; + default: + dev_warn(info->dev, "FSM meets wrong state:%d\n", + info->state); + break; + } + dev_dbg(info->dev, + "Out FSM:%s, Charger:%s, Battery:%s, Allowed:%d\n", + &fsm_state[info->state][0], + (info->online) ? "online" : "N/A", + (info->present) ? "present" : "N/A", info->allowed); + mutex_unlock(&info->lock); + + return 0; +} + +static irqreturn_t pm860x_charger_handler(int irq, void *data) +{ + struct pm860x_charger_info *info = data; + int ret; + + mutex_lock(&info->lock); + ret = pm860x_reg_read(info->i2c, PM8607_STATUS_2); + if (ret < 0) { + mutex_unlock(&info->lock); + goto out; + } + if (ret & STATUS2_CHG) { + info->online = 1; + info->allowed = 1; + } else { + info->online = 0; + info->allowed = 0; + } + mutex_unlock(&info->lock); + dev_dbg(info->dev, "%s, Charger:%s, Allowed:%d\n", __func__, + (info->online) ? "online" : "N/A", info->allowed); + + set_charging_fsm(info); + + power_supply_changed(info->usb); +out: + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_temp_handler(int irq, void *data) +{ + struct power_supply *psy; + struct pm860x_charger_info *info = data; + union power_supply_propval temp; + int value; + int ret; + + psy = power_supply_get_by_name(pm860x_supplied_to[0]); + if (!psy) + return IRQ_HANDLED; + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_TEMP, &temp); + if (ret) + goto out; + value = temp.intval / 10; + + mutex_lock(&info->lock); + /* Temperature < -10 C or >40 C, Will not allow charge */ + if (value < -10 || value > 40) + info->allowed = 0; + else + info->allowed = 1; + dev_dbg(info->dev, "%s, Allowed: %d\n", __func__, info->allowed); + mutex_unlock(&info->lock); + + set_charging_fsm(info); +out: + power_supply_put(psy); + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_exception_handler(int irq, void *data) +{ + struct pm860x_charger_info *info = data; + + mutex_lock(&info->lock); + info->allowed = 0; + mutex_unlock(&info->lock); + dev_dbg(info->dev, "%s, irq: %d\n", __func__, irq); + + set_charging_fsm(info); + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_done_handler(int irq, void *data) +{ + struct pm860x_charger_info *info = data; + struct power_supply *psy; + union power_supply_propval val; + int ret; + int vbatt; + + mutex_lock(&info->lock); + /* pre-charge done, will transimit to fast-charge stage */ + if (info->state == FSM_PRECHARGE) { + info->allowed = 1; + goto out; + } + /* + * Fast charge done, delay to read + * the correct status of CHG_DET. + */ + mdelay(5); + info->allowed = 0; + psy = power_supply_get_by_name(pm860x_supplied_to[0]); + if (!psy) + goto out; + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_VOLTAGE_NOW, + &val); + if (ret) + goto out_psy_put; + vbatt = val.intval / 1000; + /* + * CHG_DONE interrupt is faster than CHG_DET interrupt when + * plug in/out usb, So we can not rely on info->online, we + * need check pm8607 status register to check usb is online + * or not, then we can decide it is real charge done + * automatically or it is triggered by usb plug out; + */ + ret = pm860x_reg_read(info->i2c, PM8607_STATUS_2); + if (ret < 0) + goto out_psy_put; + if (vbatt > CHARGE_THRESHOLD && ret & STATUS2_CHG) + power_supply_set_property(psy, POWER_SUPPLY_PROP_CHARGE_FULL, + &val); + +out_psy_put: + power_supply_put(psy); +out: + mutex_unlock(&info->lock); + dev_dbg(info->dev, "%s, Allowed: %d\n", __func__, info->allowed); + set_charging_fsm(info); + + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_vbattery_handler(int irq, void *data) +{ + struct pm860x_charger_info *info = data; + + mutex_lock(&info->lock); + + set_vbatt_threshold(info, 0, 0); + + if (info->present && info->online) + info->allowed = 1; + else + info->allowed = 0; + mutex_unlock(&info->lock); + dev_dbg(info->dev, "%s, Allowed: %d\n", __func__, info->allowed); + + set_charging_fsm(info); + + return IRQ_HANDLED; +} + +static irqreturn_t pm860x_vchg_handler(int irq, void *data) +{ + struct pm860x_charger_info *info = data; + int vchg = 0; + + if (info->present) + goto out; + + measure_vchg(info, &vchg); + + mutex_lock(&info->lock); + if (!info->online) { + int status; + /* check if over-temp on pm8606 or not */ + status = pm860x_reg_read(info->i2c_8606, PM8606_FLAGS); + if (status & OVER_TEMP_FLAG) { + /* clear over temp flag and set auto recover */ + pm860x_set_bits(info->i2c_8606, PM8606_FLAGS, + OVER_TEMP_FLAG, OVER_TEMP_FLAG); + pm860x_set_bits(info->i2c_8606, + PM8606_VSYS, + OVTEMP_AUTORECOVER, + OVTEMP_AUTORECOVER); + dev_dbg(info->dev, + "%s, pm8606 over-temp occurred\n", __func__); + } + } + + if (vchg > VCHG_NORMAL_CHECK) { + set_vchg_threshold(info, VCHG_OVP_LOW, 0); + info->allowed = 0; + dev_dbg(info->dev, + "%s,pm8607 over-vchg occurred,vchg = %dmv\n", + __func__, vchg); + } else if (vchg < VCHG_OVP_LOW) { + set_vchg_threshold(info, VCHG_NORMAL_LOW, + VCHG_NORMAL_HIGH); + info->allowed = 1; + dev_dbg(info->dev, + "%s,pm8607 over-vchg recover,vchg = %dmv\n", + __func__, vchg); + } + mutex_unlock(&info->lock); + + dev_dbg(info->dev, "%s, Allowed: %d\n", __func__, info->allowed); + set_charging_fsm(info); +out: + return IRQ_HANDLED; +} + +static int pm860x_usb_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pm860x_charger_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (info->state == FSM_FASTCHARGE || + info->state == FSM_PRECHARGE) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = info->online; + break; + default: + return -ENODEV; + } + return 0; +} + +static enum power_supply_property pm860x_usb_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, +}; + +static int pm860x_init_charger(struct pm860x_charger_info *info) +{ + int ret; + + ret = pm860x_reg_read(info->i2c, PM8607_STATUS_2); + if (ret < 0) + return ret; + + mutex_lock(&info->lock); + info->state = FSM_INIT; + if (ret & STATUS2_CHG) { + info->online = 1; + info->allowed = 1; + } else { + info->online = 0; + info->allowed = 0; + } + mutex_unlock(&info->lock); + + set_charging_fsm(info); + return 0; +} + +static struct pm860x_irq_desc { + const char *name; + irqreturn_t (*handler)(int irq, void *data); +} pm860x_irq_descs[] = { + { "usb supply detect", pm860x_charger_handler }, + { "charge done", pm860x_done_handler }, + { "charge timeout", pm860x_exception_handler }, + { "charge fault", pm860x_exception_handler }, + { "temperature", pm860x_temp_handler }, + { "vbatt", pm860x_vbattery_handler }, + { "vchg", pm860x_vchg_handler }, +}; + +static const struct power_supply_desc pm860x_charger_desc = { + .name = "usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = pm860x_usb_props, + .num_properties = ARRAY_SIZE(pm860x_usb_props), + .get_property = pm860x_usb_get_prop, +}; + +static int pm860x_charger_probe(struct platform_device *pdev) +{ + struct pm860x_chip *chip = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + struct pm860x_charger_info *info; + int ret; + int count; + int i; + int j; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + count = pdev->num_resources; + for (i = 0, j = 0; i < count; i++) { + info->irq[j] = platform_get_irq(pdev, i); + if (info->irq[j] < 0) + continue; + j++; + } + info->irq_nums = j; + + info->chip = chip; + info->i2c = + (chip->id == CHIP_PM8607) ? chip->client : chip->companion; + info->i2c_8606 = + (chip->id == CHIP_PM8607) ? chip->companion : chip->client; + if (!info->i2c_8606) { + dev_err(&pdev->dev, "Missed I2C address of 88PM8606!\n"); + ret = -EINVAL; + goto out; + } + info->dev = &pdev->dev; + + /* set init value for the case we are not using battery */ + set_vchg_threshold(info, VCHG_NORMAL_LOW, VCHG_OVP_LOW); + + mutex_init(&info->lock); + platform_set_drvdata(pdev, info); + + psy_cfg.drv_data = info; + psy_cfg.supplied_to = pm860x_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(pm860x_supplied_to); + info->usb = power_supply_register(&pdev->dev, &pm860x_charger_desc, + &psy_cfg); + if (IS_ERR(info->usb)) { + ret = PTR_ERR(info->usb); + goto out; + } + + pm860x_init_charger(info); + + for (i = 0; i < ARRAY_SIZE(info->irq); i++) { + ret = request_threaded_irq(info->irq[i], NULL, + pm860x_irq_descs[i].handler, + IRQF_ONESHOT, pm860x_irq_descs[i].name, info); + if (ret < 0) { + dev_err(chip->dev, "Failed to request IRQ: #%d: %d\n", + info->irq[i], ret); + goto out_irq; + } + } + return 0; + +out_irq: + power_supply_unregister(info->usb); + while (--i >= 0) + free_irq(info->irq[i], info); +out: + return ret; +} + +static int pm860x_charger_remove(struct platform_device *pdev) +{ + struct pm860x_charger_info *info = platform_get_drvdata(pdev); + int i; + + power_supply_unregister(info->usb); + for (i = 0; i < info->irq_nums; i++) + free_irq(info->irq[i], info); + return 0; +} + +static struct platform_driver pm860x_charger_driver = { + .driver = { + .name = "88pm860x-charger", + }, + .probe = pm860x_charger_probe, + .remove = pm860x_charger_remove, +}; +module_platform_driver(pm860x_charger_driver); + +MODULE_DESCRIPTION("Marvell 88PM860x Charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig new file mode 100644 index 000000000..0aa46b451 --- /dev/null +++ b/drivers/power/supply/Kconfig @@ -0,0 +1,779 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig POWER_SUPPLY + bool "Power supply class support" + help + Say Y here to enable power supply class support. This allows + power supply (batteries, AC, USB) monitoring by userspace + via sysfs and uevent (if available) and/or APM kernel interface + (if selected below). + +if POWER_SUPPLY + +config POWER_SUPPLY_DEBUG + bool "Power supply debug" + help + Say Y here to enable debugging messages for power supply class + and drivers. + +config POWER_SUPPLY_HWMON + bool + prompt "Expose power supply sensors as hwmon device" + depends on HWMON=y || HWMON=POWER_SUPPLY + default y + help + This options enables API that allows sensors found on a + power supply device (current, voltage, temperature) to be + exposed as a hwmon device. + + Say 'Y' here if you want power supplies to + have hwmon sysfs interface too. + + +config PDA_POWER + tristate "Generic PDA/phone power driver" + depends on !S390 + help + Say Y here to enable generic power driver for PDAs and phones with + one or two external power supplies (AC/USB) connected to main and + backup batteries, and optional builtin charger. + +config APM_POWER + tristate "APM emulation for class batteries" + depends on APM_EMULATION + help + Say Y here to enable support APM status emulation using + battery class devices. + +config GENERIC_ADC_BATTERY + tristate "Generic battery support using IIO" + depends on IIO + help + Say Y here to enable support for the generic battery driver + which uses IIO framework to read adc. + +config MAX8925_POWER + tristate "MAX8925 battery charger support" + depends on MFD_MAX8925 + help + Say Y here to enable support for the battery charger in the Maxim + MAX8925 PMIC. + +config WM831X_BACKUP + tristate "WM831X backup battery charger support" + depends on MFD_WM831X + help + Say Y here to enable support for the backup battery charger + in the Wolfson Microelectronics WM831x PMICs. + +config WM831X_POWER + tristate "WM831X PMU support" + depends on MFD_WM831X + help + Say Y here to enable support for the power management unit + provided by Wolfson Microelectronics WM831x PMICs. + +config WM8350_POWER + tristate "WM8350 PMU support" + depends on MFD_WM8350 + help + Say Y here to enable support for the power management unit + provided by the Wolfson Microelectronics WM8350 PMIC. + +config TEST_POWER + tristate "Test power driver" + help + This driver is used for testing. It's safe to say M here. + +config BATTERY_88PM860X + tristate "Marvell 88PM860x battery driver" + depends on MFD_88PM860X + help + Say Y here to enable battery monitor for Marvell 88PM860x chip. + +config CHARGER_ADP5061 + tristate "ADP5061 battery charger driver" + depends on I2C + select REGMAP_I2C + help + Say Y here to enable support for the ADP5061 standalone battery + charger. + + This driver can be built as a module. If so, the module will be + called adp5061. + +config BATTERY_ACT8945A + tristate "Active-semi ACT8945A charger driver" + depends on MFD_ACT8945A || COMPILE_TEST + help + Say Y here to enable support for power supply provided by + Active-semi ActivePath ACT8945A charger. + +config BATTERY_CPCAP + tristate "Motorola CPCAP PMIC battery driver" + depends on MFD_CPCAP && IIO + default MFD_CPCAP + help + Say Y here to enable support for battery on Motorola + phones and tablets such as droid 4. + +config BATTERY_CW2015 + tristate "CW2015 Battery driver" + depends on I2C + select REGMAP_I2C + help + Say Y here to enable support for the cellwise cw2015 + battery fuel gauge (used in the Pinebook Pro & others) + + This driver can also be built as a module. If so, the module will be + called cw2015_battery. + +config BATTERY_DS2760 + tristate "DS2760 battery driver (HP iPAQ & others)" + depends on W1 + help + Say Y here to enable support for batteries with ds2760 chip. + +config BATTERY_DS2780 + tristate "DS2780 battery driver" + depends on HAS_IOMEM + select W1 + select W1_SLAVE_DS2780 + help + Say Y here to enable support for batteries with ds2780 chip. + +config BATTERY_DS2781 + tristate "DS2781 battery driver" + depends on HAS_IOMEM + select W1 + select W1_SLAVE_DS2781 + help + If you enable this you will have the DS2781 battery driver support. + + The battery monitor chip is used in many batteries/devices + as the one who is responsible for charging/discharging/monitoring + Li+ batteries. + + If you are unsure, say N. + +config BATTERY_DS2782 + tristate "DS2782/DS2786 standalone gas-gauge" + depends on I2C + help + Say Y here to enable support for the DS2782/DS2786 standalone battery + gas-gauge. + +config BATTERY_LEGO_EV3 + tristate "LEGO MINDSTORMS EV3 battery" + depends on OF && IIO && GPIOLIB && (ARCH_DAVINCI_DA850 || COMPILE_TEST) + help + Say Y here to enable support for the LEGO MINDSTORMS EV3 battery. + +config BATTERY_PMU + tristate "Apple PMU battery" + depends on PPC32 && ADB_PMU + help + Say Y here to expose battery information on Apple machines + through the generic battery class. + +config BATTERY_OLPC + tristate "One Laptop Per Child battery" + depends on OLPC_EC + help + Say Y to enable support for the battery on the OLPC laptop. + +config BATTERY_TOSA + tristate "Sharp SL-6000 (tosa) battery" + depends on MACH_TOSA && MFD_TC6393XB && TOUCHSCREEN_WM97XX + help + Say Y to enable support for the battery on the Sharp Zaurus + SL-6000 (tosa) models. + +config BATTERY_COLLIE + tristate "Sharp SL-5500 (collie) battery" + depends on SA1100_COLLIE && MCP_UCB1200 + help + Say Y to enable support for the battery on the Sharp Zaurus + SL-5500 (collie) models. + +config BATTERY_INGENIC + tristate "Ingenic JZ47xx SoCs battery driver" + depends on MIPS || COMPILE_TEST + depends on INGENIC_ADC + help + Choose this option if you want to monitor battery status on + Ingenic JZ47xx SoC based devices. + + This driver can also be built as a module. If so, the module will be + called ingenic-battery. + +config BATTERY_IPAQ_MICRO + tristate "iPAQ Atmel Micro ASIC battery driver" + depends on MFD_IPAQ_MICRO + help + Choose this option if you want to monitor battery status on + Compaq/HP iPAQ h3100 and h3600. + +config BATTERY_WM97XX + bool "WM97xx generic battery driver" + depends on TOUCHSCREEN_WM97XX=y + help + Say Y to enable support for battery measured by WM97xx aux port. + +config BATTERY_SBS + tristate "SBS Compliant gas gauge" + depends on I2C + help + Say Y to include support for SBS battery driver for SBS-compliant + gas gauges. + +config CHARGER_SBS + tristate "SBS Compliant charger" + depends on I2C + select REGMAP_I2C + help + Say Y to include support for SBS compliant battery chargers. + +config MANAGER_SBS + tristate "Smart Battery System Manager" + depends on I2C && I2C_MUX && GPIOLIB + select I2C_SMBUS + help + Say Y here to include support for Smart Battery System Manager + ICs. The driver reports online and charging status via sysfs. + It presents itself also as I2C mux which allows to bind + smart battery driver to its ports. + Supported is for example LTC1760. + + This driver can also be built as a module. If so, the module will be + called sbs-manager. + +config BATTERY_BQ27XXX + tristate "BQ27xxx battery driver" + help + Say Y here to enable support for batteries with BQ27xxx chips. + +config BATTERY_BQ27XXX_I2C + tristate "BQ27xxx I2C support" + depends on BATTERY_BQ27XXX + depends on I2C + default y + help + Say Y here to enable support for batteries with BQ27xxx chips + connected over an I2C bus. + +config BATTERY_BQ27XXX_HDQ + tristate "BQ27xxx HDQ support" + depends on BATTERY_BQ27XXX + depends on W1 + default y + help + Say Y here to enable support for batteries with BQ27xxx chips + connected over an HDQ bus. + +config BATTERY_BQ27XXX_DT_UPDATES_NVM + bool "BQ27xxx support for update of NVM/flash data memory" + depends on BATTERY_BQ27XXX_I2C + help + Say Y here to enable devicetree monitored-battery config to update + NVM/flash data memory. Only enable this option for devices with a + fuel gauge mounted on the circuit board, and a battery that cannot + easily be replaced with one of a different type. Not for + general-purpose kernels, as this can cause misconfiguration of a + smart battery with embedded NVM/flash. + +config BATTERY_DA9030 + tristate "DA9030 battery driver" + depends on PMIC_DA903X + help + Say Y here to enable support for batteries charger integrated into + DA9030 PMIC. + +config BATTERY_DA9052 + tristate "Dialog DA9052 Battery" + depends on PMIC_DA9052 + help + Say Y here to enable support for batteries charger integrated into + DA9052 PMIC. + +config CHARGER_DA9150 + tristate "Dialog Semiconductor DA9150 Charger support" + depends on MFD_DA9150 + depends on DA9150_GPADC + depends on IIO + help + Say Y here to enable support for charger unit of the DA9150 + Integrated Charger & Fuel-Gauge IC. + + This driver can also be built as a module. If so, the module will be + called da9150-charger. + +config BATTERY_DA9150 + tristate "Dialog Semiconductor DA9150 Fuel Gauge support" + depends on MFD_DA9150 + help + Say Y here to enable support for the Fuel-Gauge unit of the DA9150 + Integrated Charger & Fuel-Gauge IC + + This driver can also be built as a module. If so, the module will be + called da9150-fg. + +config CHARGER_AXP20X + tristate "X-Powers AXP20X and AXP22X AC power supply driver" + depends on MFD_AXP20X + depends on AXP20X_ADC + depends on IIO + help + Say Y here to enable support for X-Powers AXP20X and AXP22X PMICs' AC + power supply. + + This driver can also be built as a module. If so, the module will be + called axp20x_ac_power. + +config BATTERY_AXP20X + tristate "X-Powers AXP20X battery driver" + depends on MFD_AXP20X + depends on AXP20X_ADC + depends on IIO + help + Say Y here to enable support for X-Powers AXP20X PMICs' battery power + supply. + + This driver can also be built as a module. If so, the module will be + called axp20x_battery. + +config AXP20X_POWER + tristate "AXP20x power supply driver" + depends on MFD_AXP20X + depends on IIO + help + This driver provides support for the power supply features of + AXP20x PMIC. + +config AXP288_CHARGER + tristate "X-Powers AXP288 Charger" + depends on MFD_AXP20X && EXTCON_AXP288 + help + Say yes here to have support X-Power AXP288 power management IC (PMIC) + integrated charger. + +config AXP288_FUEL_GAUGE + tristate "X-Powers AXP288 Fuel Gauge" + depends on MFD_AXP20X && IIO + help + Say yes here to have support for X-Power power management IC (PMIC) + Fuel Gauge. The device provides battery statistics and status + monitoring as well as alerts for battery over/under voltage and + over/under temperature. + +config BATTERY_MAX17040 + tristate "Maxim MAX17040 Fuel Gauge" + depends on I2C + select REGMAP_I2C + help + Maxim models with ModelGauge are fuel-gauge systems for lithium-ion + (Li+) batteries in handheld and portable equipment, including + max17040, max17041, max17043, max17044, max17048, max17049, max17058, + max17059. It is also included in some batteries like max77836. + + Driver supports reporting SOC (State of Charge, i.e capacity), + voltage and configurable low-SOC wakeup interrupt. + +config BATTERY_MAX17042 + tristate "Maxim MAX17042/17047/17050/8997/8966 Fuel Gauge" + depends on I2C + select REGMAP_I2C + help + MAX17042 is fuel-gauge systems for lithium-ion (Li+) batteries + in handheld and portable equipment. The MAX17042 is configured + to operate with a single lithium cell. MAX8997 and MAX8966 are + multi-function devices that include fuel gauages that are compatible + with MAX17042. This driver also supports max17047/50 chips which are + improved version of max17042. + +config BATTERY_MAX1721X + tristate "MAX17211/MAX17215 standalone gas-gauge" + depends on W1 + select REGMAP_W1 + help + MAX1721x is fuel-gauge systems for lithium-ion (Li+) batteries + in handheld and portable equipment. MAX17211 used with single cell + battery. MAX17215 designed for muticell battery. Both them have + OneWire (W1) host interface. + + Say Y here to enable support for the MAX17211/MAX17215 standalone + battery gas-gauge. + +config BATTERY_Z2 + tristate "Z2 battery driver" + depends on I2C && MACH_ZIPIT2 + help + Say Y to include support for the battery on the Zipit Z2. + +config BATTERY_S3C_ADC + tristate "Battery driver for Samsung ADC based monitoring" + depends on S3C_ADC + help + Say Y here to enable support for iPAQ h1930/h1940/rx1950 battery + +config BATTERY_TWL4030_MADC + tristate "TWL4030 MADC battery driver" + depends on TWL4030_MADC + help + Say Y here to enable this dumb driver for batteries managed + through the TWL4030 MADC. + +config CHARGER_88PM860X + tristate "Marvell 88PM860x Charger driver" + depends on MFD_88PM860X && BATTERY_88PM860X + help + Say Y here to enable charger for Marvell 88PM860x chip. + +config CHARGER_PCF50633 + tristate "NXP PCF50633 MBC" + depends on MFD_PCF50633 + help + Say Y to include support for NXP PCF50633 Main Battery Charger. + +config BATTERY_RX51 + tristate "Nokia RX-51 (N900) battery driver" + depends on TWL4030_MADC + help + Say Y here to enable support for battery information on Nokia + RX-51, also known as N900 tablet. + +config CHARGER_CPCAP + tristate "CPCAP PMIC Charger Driver" + depends on MFD_CPCAP && IIO + depends on OMAP_USB2 || (!OMAP_USB2 && COMPILE_TEST) + default MFD_CPCAP + help + Say Y to enable support for CPCAP PMIC charger driver for Motorola + mobile devices such as Droid 4. + +config CHARGER_ISP1704 + tristate "ISP1704 USB Charger Detection" + depends on USB_PHY + depends on USB_GADGET || !USB_GADGET # if USB_GADGET=m, this can't be 'y' + help + Say Y to enable support for USB Charger Detection with + ISP1707/ISP1704 USB transceivers. + +config CHARGER_MAX8903 + tristate "MAX8903 Battery DC-DC Charger for USB and Adapter Power" + help + Say Y to enable support for the MAX8903 DC-DC charger and sysfs. + The driver supports controlling charger-enable and current-limit + pins based on the status of charger connections with interrupt + handlers. + +config CHARGER_TWL4030 + tristate "OMAP TWL4030 BCI charger driver" + depends on IIO && TWL4030_CORE + help + Say Y here to enable support for TWL4030 Battery Charge Interface. + +config CHARGER_LP8727 + tristate "TI/National Semiconductor LP8727 charger driver" + depends on I2C + help + Say Y here to enable support for LP8727 Charger Driver. + +config CHARGER_LP8788 + tristate "TI LP8788 charger driver" + depends on MFD_LP8788 + depends on LP8788_ADC + depends on IIO + help + Say Y to enable support for the LP8788 linear charger. + +config CHARGER_GPIO + tristate "GPIO charger" + depends on GPIOLIB || COMPILE_TEST + help + Say Y to include support for chargers which report their online status + through a GPIO pin. + + This driver can be build as a module. If so, the module will be + called gpio-charger. + +config CHARGER_MANAGER + tristate "Battery charger manager for multiple chargers" + depends on REGULATOR + select EXTCON + help + Say Y to enable charger-manager support, which allows multiple + chargers attached to a battery and multiple batteries attached to a + system. The charger-manager also can monitor charging status in + runtime and in suspend-to-RAM by waking up the system periodically + with help of suspend_again support. + +config CHARGER_LT3651 + tristate "Analog Devices LT3651 charger" + depends on GPIOLIB + help + Say Y to include support for the Analog Devices (Linear Technology) + LT3651 battery charger which reports its status via GPIO lines. + +config CHARGER_MAX14577 + tristate "Maxim MAX14577/77836 battery charger driver" + depends on MFD_MAX14577 + help + Say Y to enable support for the battery charger control sysfs and + platform data of MAX14577/77836 MUICs. + +config CHARGER_DETECTOR_MAX14656 + tristate "Maxim MAX14656 USB charger detector" + depends on I2C + depends on OF + help + Say Y to enable support for the Maxim MAX14656 USB charger detector. + The device is compliant with the USB Battery Charging Specification + Revision 1.2 and can be found e.g. in Kindle 4/5th generation + readers and certain LG devices. + +config CHARGER_MAX77650 + tristate "Maxim MAX77650 battery charger driver" + depends on MFD_MAX77650 + help + Say Y to enable support for the battery charger control of MAX77650 + PMICs. + +config CHARGER_MAX77693 + tristate "Maxim MAX77693 battery charger driver" + depends on MFD_MAX77693 + help + Say Y to enable support for the Maxim MAX77693 battery charger. + +config CHARGER_MAX8997 + tristate "Maxim MAX8997/MAX8966 PMIC battery charger driver" + depends on MFD_MAX8997 && REGULATOR_MAX8997 + help + Say Y to enable support for the battery charger control sysfs and + platform data of MAX8997/LP3974 PMICs. + +config CHARGER_MAX8998 + tristate "Maxim MAX8998/LP3974 PMIC battery charger driver" + depends on MFD_MAX8998 && REGULATOR_MAX8998 + help + Say Y to enable support for the battery charger control sysfs and + platform data of MAX8998/LP3974 PMICs. + +config CHARGER_MP2629 + tristate "Monolithic power system MP2629 Battery charger" + depends on MFD_MP2629 + depends on MP2629_ADC + depends on IIO + help + Select this option to enable support for Monolithic power system + Battery charger. This driver provides Battery charger power management + functions on the systems. + +config CHARGER_QCOM_SMBB + tristate "Qualcomm Switch-Mode Battery Charger and Boost" + depends on MFD_SPMI_PMIC || COMPILE_TEST + depends on OF + depends on EXTCON + depends on REGULATOR + help + Say Y to include support for the Switch-Mode Battery Charger and + Boost (SMBB) hardware found in Qualcomm PM8941 PMICs. The charger + is an integrated, single-cell lithium-ion battery charger. DT + configuration is required for loading, see the devicetree + documentation for more detail. The base name for this driver is + 'pm8941_charger'. + +config CHARGER_BQ2415X + tristate "TI BQ2415x battery charger driver" + depends on I2C + help + Say Y to enable support for the TI BQ2415x battery charger + PMICs. + + You'll need this driver to charge batteries on e.g. Nokia + RX-51/N900. + +config CHARGER_BQ24190 + tristate "TI BQ24190 battery charger driver" + depends on I2C + depends on EXTCON + depends on GPIOLIB || COMPILE_TEST + help + Say Y to enable support for the TI BQ24190 battery charger. + +config CHARGER_BQ24257 + tristate "TI BQ24250/24251/24257 battery charger driver" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + select REGMAP_I2C + help + Say Y to enable support for the TI BQ24250, BQ24251, and BQ24257 battery + chargers. + +config CHARGER_BQ24735 + tristate "TI BQ24735 battery charger support" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + help + Say Y to enable support for the TI BQ24735 battery charger. + +config CHARGER_BQ2515X + tristate "TI BQ2515X battery charger family" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + select REGMAP_I2C + help + Say Y to enable support for the TI BQ2515X family of battery + charging integrated circuits. The BQ2515X are highly integrated + battery charge management ICs that integrate the most common + functions for wearable devices, namely a charger, an output voltage + rail, ADC for battery and system monitoring, and push-button + controller. + +config CHARGER_BQ25890 + tristate "TI BQ25890 battery charger driver" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + select REGMAP_I2C + help + Say Y to enable support for the TI BQ25890 battery charger. + +config CHARGER_BQ25980 + tristate "TI BQ25980 battery charger driver" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + select REGMAP_I2C + help + Say Y to enable support for the TI BQ25980, BQ25975 and BQ25960 + series of fast battery chargers. + +config CHARGER_SMB347 + tristate "Summit Microelectronics SMB3XX Battery Charger" + depends on I2C + select REGMAP_I2C + help + Say Y to include support for Summit Microelectronics SMB345, + SMB347 or SMB358 Battery Charger. + +config CHARGER_TPS65090 + tristate "TPS65090 battery charger driver" + depends on MFD_TPS65090 + help + Say Y here to enable support for battery charging with TPS65090 + PMIC chips. + +config CHARGER_TPS65217 + tristate "TPS65217 battery charger driver" + depends on MFD_TPS65217 + help + Say Y here to enable support for battery charging with TPS65217 + PMIC chips. + +config BATTERY_GAUGE_LTC2941 + tristate "LTC2941/LTC2943 Battery Gauge Driver" + depends on I2C + help + Say Y here to include support for LTC2941 and LTC2943 Battery + Gauge IC. The driver reports the charge count continuously, and + measures the voltage and temperature every 10 seconds. + +config AB8500_BM + bool "AB8500 Battery Management Driver" + depends on AB8500_CORE && AB8500_GPADC && (IIO = y) + help + Say Y to include support for AB8500 battery management. + +config BATTERY_GOLDFISH + tristate "Goldfish battery driver" + depends on GOLDFISH || COMPILE_TEST + depends on HAS_IOMEM + help + Say Y to enable support for the battery and AC power in the + Goldfish emulator. + +config BATTERY_RT5033 + tristate "RT5033 fuel gauge support" + depends on I2C + select REGMAP_I2C + help + This adds support for battery fuel gauge in Richtek RT5033 PMIC. + The fuelgauge calculates and determines the battery state of charge + according to battery open circuit voltage. + +config CHARGER_RT9455 + tristate "Richtek RT9455 battery charger driver" + depends on I2C + depends on GPIOLIB || COMPILE_TEST + select REGMAP_I2C + help + Say Y to enable support for Richtek RT9455 battery charger. + +config CHARGER_CROS_USBPD + tristate "ChromeOS EC based USBPD charger" + depends on CROS_USBPD_NOTIFY + help + Say Y here to enable ChromeOS EC based USBPD charger + driver. This driver gets various bits of information about + what is connected to USB PD ports from the EC and converts + that into power_supply properties. + +config CHARGER_SC2731 + tristate "Spreadtrum SC2731 charger driver" + depends on MFD_SC27XX_PMIC || COMPILE_TEST + help + Say Y here to enable support for battery charging with SC2731 + PMIC chips. + +config FUEL_GAUGE_SC27XX + tristate "Spreadtrum SC27XX fuel gauge driver" + depends on MFD_SC27XX_PMIC || COMPILE_TEST + depends on IIO + help + Say Y here to enable support for fuel gauge with SC27XX + PMIC chips. + +config CHARGER_UCS1002 + tristate "Microchip UCS1002 USB Port Power Controller" + depends on I2C + depends on OF + depends on REGULATOR + select REGMAP_I2C + help + Say Y to enable support for Microchip UCS1002 Programmable + USB Port Power Controller with Charger Emulation. + +config CHARGER_BD70528 + tristate "ROHM bd70528 charger driver" + depends on MFD_ROHM_BD70528 + select LINEAR_RANGES + help + Say Y here to enable support for getting battery status + information and altering charger configurations from charger + block of the ROHM BD70528 Power Management IC. + +config CHARGER_BD99954 + tristate "ROHM bd99954 charger driver" + depends on I2C + select LINEAR_RANGES + help + Say Y here to enable support for getting battery and charger + information and altering charger configurations from the ROHM + BD99954 charger IC. + +config CHARGER_WILCO + tristate "Wilco EC based charger for ChromeOS" + depends on WILCO_EC + help + Say Y here to enable control of the charging routines performed + by the Embedded Controller on the Chromebook named Wilco. Further + information can be found in + Documentation/ABI/testing/sysfs-class-power-wilco + +config RN5T618_POWER + tristate "RN5T618 charger/fuel gauge support" + depends on MFD_RN5T618 + help + Say Y here to have support for RN5T618 PMIC family fuel gauge and charger. + This driver can also be built as a module. If so, the module will be + called rn5t618_power. + +endif # POWER_SUPPLY diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile new file mode 100644 index 000000000..dd4b86318 --- /dev/null +++ b/drivers/power/supply/Makefile @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: GPL-2.0 +subdir-ccflags-$(CONFIG_POWER_SUPPLY_DEBUG) := -DDEBUG + +power_supply-y := power_supply_core.o +power_supply-$(CONFIG_SYSFS) += power_supply_sysfs.o +power_supply-$(CONFIG_LEDS_TRIGGERS) += power_supply_leds.o + +obj-$(CONFIG_POWER_SUPPLY) += power_supply.o +obj-$(CONFIG_POWER_SUPPLY_HWMON) += power_supply_hwmon.o +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_MAX8925_POWER) += max8925_power.o +obj-$(CONFIG_WM831X_BACKUP) += wm831x_backup.o +obj-$(CONFIG_WM831X_POWER) += wm831x_power.o +obj-$(CONFIG_WM8350_POWER) += wm8350_power.o +obj-$(CONFIG_TEST_POWER) += test_power.o + +obj-$(CONFIG_BATTERY_88PM860X) += 88pm860x_battery.o +obj-$(CONFIG_CHARGER_ADP5061) += adp5061.o +obj-$(CONFIG_BATTERY_ACT8945A) += act8945a_charger.o +obj-$(CONFIG_BATTERY_AXP20X) += axp20x_battery.o +obj-$(CONFIG_CHARGER_AXP20X) += axp20x_ac_power.o +obj-$(CONFIG_BATTERY_CPCAP) += cpcap-battery.o +obj-$(CONFIG_BATTERY_CW2015) += cw2015_battery.o +obj-$(CONFIG_BATTERY_DS2760) += ds2760_battery.o +obj-$(CONFIG_BATTERY_DS2780) += ds2780_battery.o +obj-$(CONFIG_BATTERY_DS2781) += ds2781_battery.o +obj-$(CONFIG_BATTERY_DS2782) += ds2782_battery.o +obj-$(CONFIG_BATTERY_GAUGE_LTC2941) += ltc2941-battery-gauge.o +obj-$(CONFIG_BATTERY_GOLDFISH) += goldfish_battery.o +obj-$(CONFIG_BATTERY_LEGO_EV3) += lego_ev3_battery.o +obj-$(CONFIG_BATTERY_PMU) += pmu_battery.o +obj-$(CONFIG_BATTERY_OLPC) += olpc_battery.o +obj-$(CONFIG_BATTERY_TOSA) += tosa_battery.o +obj-$(CONFIG_BATTERY_COLLIE) += collie_battery.o +obj-$(CONFIG_BATTERY_INGENIC) += ingenic-battery.o +obj-$(CONFIG_BATTERY_IPAQ_MICRO) += ipaq_micro_battery.o +obj-$(CONFIG_BATTERY_WM97XX) += wm97xx_battery.o +obj-$(CONFIG_BATTERY_SBS) += sbs-battery.o +obj-$(CONFIG_CHARGER_SBS) += sbs-charger.o +obj-$(CONFIG_MANAGER_SBS) += sbs-manager.o +obj-$(CONFIG_BATTERY_BQ27XXX) += bq27xxx_battery.o +obj-$(CONFIG_BATTERY_BQ27XXX_I2C) += bq27xxx_battery_i2c.o +obj-$(CONFIG_BATTERY_BQ27XXX_HDQ) += bq27xxx_battery_hdq.o +obj-$(CONFIG_BATTERY_DA9030) += da9030_battery.o +obj-$(CONFIG_BATTERY_DA9052) += da9052-battery.o +obj-$(CONFIG_CHARGER_DA9150) += da9150-charger.o +obj-$(CONFIG_BATTERY_DA9150) += da9150-fg.o +obj-$(CONFIG_BATTERY_MAX17040) += max17040_battery.o +obj-$(CONFIG_BATTERY_MAX17042) += max17042_battery.o +obj-$(CONFIG_BATTERY_MAX1721X) += max1721x_battery.o +obj-$(CONFIG_BATTERY_Z2) += z2_battery.o +obj-$(CONFIG_BATTERY_RT5033) += rt5033_battery.o +obj-$(CONFIG_CHARGER_RT9455) += rt9455_charger.o +obj-$(CONFIG_BATTERY_S3C_ADC) += s3c_adc_battery.o +obj-$(CONFIG_BATTERY_TWL4030_MADC) += twl4030_madc_battery.o +obj-$(CONFIG_CHARGER_88PM860X) += 88pm860x_charger.o +obj-$(CONFIG_CHARGER_PCF50633) += pcf50633-charger.o +obj-$(CONFIG_BATTERY_RX51) += rx51_battery.o +obj-$(CONFIG_AB8500_BM) += ab8500_bmdata.o ab8500_charger.o ab8500_fg.o ab8500_btemp.o abx500_chargalg.o pm2301_charger.o +obj-$(CONFIG_CHARGER_CPCAP) += cpcap-charger.o +obj-$(CONFIG_CHARGER_ISP1704) += isp1704_charger.o +obj-$(CONFIG_CHARGER_MAX8903) += max8903_charger.o +obj-$(CONFIG_CHARGER_TWL4030) += twl4030_charger.o +obj-$(CONFIG_CHARGER_LP8727) += lp8727_charger.o +obj-$(CONFIG_CHARGER_LP8788) += lp8788-charger.o +obj-$(CONFIG_CHARGER_GPIO) += gpio-charger.o +obj-$(CONFIG_CHARGER_MANAGER) += charger-manager.o +obj-$(CONFIG_CHARGER_LT3651) += lt3651-charger.o +obj-$(CONFIG_CHARGER_MAX14577) += max14577_charger.o +obj-$(CONFIG_CHARGER_DETECTOR_MAX14656) += max14656_charger_detector.o +obj-$(CONFIG_CHARGER_MAX77650) += max77650-charger.o +obj-$(CONFIG_CHARGER_MAX77693) += max77693_charger.o +obj-$(CONFIG_CHARGER_MAX8997) += max8997_charger.o +obj-$(CONFIG_CHARGER_MAX8998) += max8998_charger.o +obj-$(CONFIG_CHARGER_MP2629) += mp2629_charger.o +obj-$(CONFIG_CHARGER_QCOM_SMBB) += qcom_smbb.o +obj-$(CONFIG_CHARGER_BQ2415X) += bq2415x_charger.o +obj-$(CONFIG_CHARGER_BQ24190) += bq24190_charger.o +obj-$(CONFIG_CHARGER_BQ24257) += bq24257_charger.o +obj-$(CONFIG_CHARGER_BQ24735) += bq24735-charger.o +obj-$(CONFIG_CHARGER_BQ2515X) += bq2515x_charger.o +obj-$(CONFIG_CHARGER_BQ25890) += bq25890_charger.o +obj-$(CONFIG_CHARGER_BQ25980) += bq25980_charger.o +obj-$(CONFIG_CHARGER_SMB347) += smb347-charger.o +obj-$(CONFIG_CHARGER_TPS65090) += tps65090-charger.o +obj-$(CONFIG_CHARGER_TPS65217) += tps65217_charger.o +obj-$(CONFIG_AXP288_FUEL_GAUGE) += axp288_fuel_gauge.o +obj-$(CONFIG_AXP288_CHARGER) += axp288_charger.o +obj-$(CONFIG_CHARGER_CROS_USBPD) += cros_usbpd-charger.o +obj-$(CONFIG_CHARGER_SC2731) += sc2731_charger.o +obj-$(CONFIG_FUEL_GAUGE_SC27XX) += sc27xx_fuel_gauge.o +obj-$(CONFIG_CHARGER_UCS1002) += ucs1002_power.o +obj-$(CONFIG_CHARGER_BD70528) += bd70528-charger.o +obj-$(CONFIG_CHARGER_BD99954) += bd99954-charger.o +obj-$(CONFIG_CHARGER_WILCO) += wilco-charger.o +obj-$(CONFIG_RN5T618_POWER) += rn5t618_power.o diff --git a/drivers/power/supply/ab8500_bmdata.c b/drivers/power/supply/ab8500_bmdata.c new file mode 100644 index 000000000..f6a66979c --- /dev/null +++ b/drivers/power/supply/ab8500_bmdata.c @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/export.h> +#include <linux/power_supply.h> +#include <linux/of.h> +#include <linux/mfd/abx500.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500/ab8500-bm.h> + +/* + * These are the defined batteries that uses a NTC and ID resistor placed + * inside of the battery pack. + * Note that the res_to_temp table must be strictly sorted by falling resistance + * values to work. + */ +const struct abx500_res_to_temp ab8500_temp_tbl_a_thermistor[] = { + {-5, 53407}, + { 0, 48594}, + { 5, 43804}, + {10, 39188}, + {15, 34870}, + {20, 30933}, + {25, 27422}, + {30, 24347}, + {35, 21694}, + {40, 19431}, + {45, 17517}, + {50, 15908}, + {55, 14561}, + {60, 13437}, + {65, 12500}, +}; +EXPORT_SYMBOL(ab8500_temp_tbl_a_thermistor); + +const int ab8500_temp_tbl_a_size = ARRAY_SIZE(ab8500_temp_tbl_a_thermistor); +EXPORT_SYMBOL(ab8500_temp_tbl_a_size); + +const struct abx500_res_to_temp ab8500_temp_tbl_b_thermistor[] = { + {-5, 200000}, + { 0, 159024}, + { 5, 151921}, + {10, 144300}, + {15, 136424}, + {20, 128565}, + {25, 120978}, + {30, 113875}, + {35, 107397}, + {40, 101629}, + {45, 96592}, + {50, 92253}, + {55, 88569}, + {60, 85461}, + {65, 82869}, +}; +EXPORT_SYMBOL(ab8500_temp_tbl_b_thermistor); + +const int ab8500_temp_tbl_b_size = ARRAY_SIZE(ab8500_temp_tbl_b_thermistor); +EXPORT_SYMBOL(ab8500_temp_tbl_b_size); + +static const struct abx500_v_to_cap cap_tbl_a_thermistor[] = { + {4171, 100}, + {4114, 95}, + {4009, 83}, + {3947, 74}, + {3907, 67}, + {3863, 59}, + {3830, 56}, + {3813, 53}, + {3791, 46}, + {3771, 33}, + {3754, 25}, + {3735, 20}, + {3717, 17}, + {3681, 13}, + {3664, 8}, + {3651, 6}, + {3635, 5}, + {3560, 3}, + {3408, 1}, + {3247, 0}, +}; + +static const struct abx500_v_to_cap cap_tbl_b_thermistor[] = { + {4161, 100}, + {4124, 98}, + {4044, 90}, + {4003, 85}, + {3966, 80}, + {3933, 75}, + {3888, 67}, + {3849, 60}, + {3813, 55}, + {3787, 47}, + {3772, 30}, + {3751, 25}, + {3718, 20}, + {3681, 16}, + {3660, 14}, + {3589, 10}, + {3546, 7}, + {3495, 4}, + {3404, 2}, + {3250, 0}, +}; + +static const struct abx500_v_to_cap cap_tbl[] = { + {4186, 100}, + {4163, 99}, + {4114, 95}, + {4068, 90}, + {3990, 80}, + {3926, 70}, + {3898, 65}, + {3866, 60}, + {3833, 55}, + {3812, 50}, + {3787, 40}, + {3768, 30}, + {3747, 25}, + {3730, 20}, + {3705, 15}, + {3699, 14}, + {3684, 12}, + {3672, 9}, + {3657, 7}, + {3638, 6}, + {3556, 4}, + {3424, 2}, + {3317, 1}, + {3094, 0}, +}; + +/* + * Note that the res_to_temp table must be strictly sorted by falling + * resistance values to work. + */ +static const struct abx500_res_to_temp temp_tbl[] = { + {-5, 214834}, + { 0, 162943}, + { 5, 124820}, + {10, 96520}, + {15, 75306}, + {20, 59254}, + {25, 47000}, + {30, 37566}, + {35, 30245}, + {40, 24520}, + {45, 20010}, + {50, 16432}, + {55, 13576}, + {60, 11280}, + {65, 9425}, +}; + +/* + * Note that the batres_vs_temp table must be strictly sorted by falling + * temperature values to work. + */ +static const struct batres_vs_temp temp_to_batres_tbl_thermistor[] = { + { 40, 120}, + { 30, 135}, + { 20, 165}, + { 10, 230}, + { 00, 325}, + {-10, 445}, + {-20, 595}, +}; + +/* + * Note that the batres_vs_temp table must be strictly sorted by falling + * temperature values to work. + */ +static const struct batres_vs_temp temp_to_batres_tbl_ext_thermistor[] = { + { 60, 300}, + { 30, 300}, + { 20, 300}, + { 10, 300}, + { 00, 300}, + {-10, 300}, + {-20, 300}, +}; + +/* battery resistance table for LI ION 9100 battery */ +static const struct batres_vs_temp temp_to_batres_tbl_9100[] = { + { 60, 180}, + { 30, 180}, + { 20, 180}, + { 10, 180}, + { 00, 180}, + {-10, 180}, + {-20, 180}, +}; + +static struct abx500_battery_type bat_type_thermistor[] = { + [BATTERY_UNKNOWN] = { + /* First element always represent the UNKNOWN battery */ + .name = POWER_SUPPLY_TECHNOLOGY_UNKNOWN, + .resis_high = 0, + .resis_low = 0, + .battery_resistance = 300, + .charge_full_design = 612, + .nominal_voltage = 3700, + .termination_vol = 4050, + .termination_curr = 200, + .recharge_cap = 95, + .normal_cur_lvl = 400, + .normal_vol_lvl = 4100, + .maint_a_cur_lvl = 400, + .maint_a_vol_lvl = 4050, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 400, + .maint_b_vol_lvl = 4000, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(temp_tbl), + .r_to_t_tbl = temp_tbl, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl), + .v_to_cap_tbl = cap_tbl, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, + { + .name = POWER_SUPPLY_TECHNOLOGY_LIPO, + .resis_high = 53407, + .resis_low = 12500, + .battery_resistance = 300, + .charge_full_design = 900, + .nominal_voltage = 3600, + .termination_vol = 4150, + .termination_curr = 80, + .recharge_cap = 95, + .normal_cur_lvl = 700, + .normal_vol_lvl = 4200, + .maint_a_cur_lvl = 600, + .maint_a_vol_lvl = 4150, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 600, + .maint_b_vol_lvl = 4100, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(ab8500_temp_tbl_a_thermistor), + .r_to_t_tbl = ab8500_temp_tbl_a_thermistor, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl_a_thermistor), + .v_to_cap_tbl = cap_tbl_a_thermistor, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + + }, + { + .name = POWER_SUPPLY_TECHNOLOGY_LIPO, + .resis_high = 200000, + .resis_low = 82869, + .battery_resistance = 300, + .charge_full_design = 900, + .nominal_voltage = 3600, + .termination_vol = 4150, + .termination_curr = 80, + .recharge_cap = 95, + .normal_cur_lvl = 700, + .normal_vol_lvl = 4200, + .maint_a_cur_lvl = 600, + .maint_a_vol_lvl = 4150, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 600, + .maint_b_vol_lvl = 4100, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(ab8500_temp_tbl_b_thermistor), + .r_to_t_tbl = ab8500_temp_tbl_b_thermistor, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl_b_thermistor), + .v_to_cap_tbl = cap_tbl_b_thermistor, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, +}; + +static struct abx500_battery_type bat_type_ext_thermistor[] = { + [BATTERY_UNKNOWN] = { + /* First element always represent the UNKNOWN battery */ + .name = POWER_SUPPLY_TECHNOLOGY_UNKNOWN, + .resis_high = 0, + .resis_low = 0, + .battery_resistance = 300, + .charge_full_design = 612, + .nominal_voltage = 3700, + .termination_vol = 4050, + .termination_curr = 200, + .recharge_cap = 95, + .normal_cur_lvl = 400, + .normal_vol_lvl = 4100, + .maint_a_cur_lvl = 400, + .maint_a_vol_lvl = 4050, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 400, + .maint_b_vol_lvl = 4000, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(temp_tbl), + .r_to_t_tbl = temp_tbl, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl), + .v_to_cap_tbl = cap_tbl, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, +/* + * These are the batteries that doesn't have an internal NTC resistor to measure + * its temperature. The temperature in this case is measure with a NTC placed + * near the battery but on the PCB. + */ + { + .name = POWER_SUPPLY_TECHNOLOGY_LIPO, + .resis_high = 76000, + .resis_low = 53000, + .battery_resistance = 300, + .charge_full_design = 900, + .nominal_voltage = 3700, + .termination_vol = 4150, + .termination_curr = 100, + .recharge_cap = 95, + .normal_cur_lvl = 700, + .normal_vol_lvl = 4200, + .maint_a_cur_lvl = 600, + .maint_a_vol_lvl = 4150, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 600, + .maint_b_vol_lvl = 4100, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(temp_tbl), + .r_to_t_tbl = temp_tbl, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl), + .v_to_cap_tbl = cap_tbl, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, + { + .name = POWER_SUPPLY_TECHNOLOGY_LION, + .resis_high = 30000, + .resis_low = 10000, + .battery_resistance = 300, + .charge_full_design = 950, + .nominal_voltage = 3700, + .termination_vol = 4150, + .termination_curr = 100, + .recharge_cap = 95, + .normal_cur_lvl = 700, + .normal_vol_lvl = 4200, + .maint_a_cur_lvl = 600, + .maint_a_vol_lvl = 4150, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 600, + .maint_b_vol_lvl = 4100, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(temp_tbl), + .r_to_t_tbl = temp_tbl, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl), + .v_to_cap_tbl = cap_tbl, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, + { + .name = POWER_SUPPLY_TECHNOLOGY_LION, + .resis_high = 95000, + .resis_low = 76001, + .battery_resistance = 300, + .charge_full_design = 950, + .nominal_voltage = 3700, + .termination_vol = 4150, + .termination_curr = 100, + .recharge_cap = 95, + .normal_cur_lvl = 700, + .normal_vol_lvl = 4200, + .maint_a_cur_lvl = 600, + .maint_a_vol_lvl = 4150, + .maint_a_chg_timer_h = 60, + .maint_b_cur_lvl = 600, + .maint_b_vol_lvl = 4100, + .maint_b_chg_timer_h = 200, + .low_high_cur_lvl = 300, + .low_high_vol_lvl = 4000, + .n_temp_tbl_elements = ARRAY_SIZE(temp_tbl), + .r_to_t_tbl = temp_tbl, + .n_v_cap_tbl_elements = ARRAY_SIZE(cap_tbl), + .v_to_cap_tbl = cap_tbl, + .n_batres_tbl_elements = ARRAY_SIZE(temp_to_batres_tbl_thermistor), + .batres_tbl = temp_to_batres_tbl_thermistor, + }, +}; + +static const struct abx500_bm_capacity_levels cap_levels = { + .critical = 2, + .low = 10, + .normal = 70, + .high = 95, + .full = 100, +}; + +static const struct abx500_fg_parameters fg = { + .recovery_sleep_timer = 10, + .recovery_total_time = 100, + .init_timer = 1, + .init_discard_time = 5, + .init_total_time = 40, + .high_curr_time = 60, + .accu_charging = 30, + .accu_high_curr = 30, + .high_curr_threshold = 50, + .lowbat_threshold = 3100, + .battok_falling_th_sel0 = 2860, + .battok_raising_th_sel1 = 2860, + .maint_thres = 95, + .user_cap_limit = 15, + .pcut_enable = 1, + .pcut_max_time = 127, + .pcut_flag_time = 112, + .pcut_max_restart = 15, + .pcut_debounce_time = 2, +}; + +static const struct abx500_maxim_parameters ab8500_maxi_params = { + .ena_maxi = true, + .chg_curr = 910, + .wait_cycles = 10, + .charger_curr_step = 100, +}; + +static const struct abx500_bm_charger_parameters chg = { + .usb_volt_max = 5500, + .usb_curr_max = 1500, + .ac_volt_max = 7500, + .ac_curr_max = 1500, +}; + +/* + * This array maps the raw hex value to charger output current used by the + * AB8500 values + */ +static int ab8500_charge_output_curr_map[] = { + 100, 200, 300, 400, 500, 600, 700, 800, + 900, 1000, 1100, 1200, 1300, 1400, 1500, 1500, +}; + +/* + * This array maps the raw hex value to charger input current used by the + * AB8500 values + */ +static int ab8500_charge_input_curr_map[] = { + 50, 98, 193, 290, 380, 450, 500, 600, + 700, 800, 900, 1000, 1100, 1300, 1400, 1500, +}; + +struct abx500_bm_data ab8500_bm_data = { + .temp_under = 3, + .temp_low = 8, + .temp_high = 43, + .temp_over = 48, + .main_safety_tmr_h = 4, + .temp_interval_chg = 20, + .temp_interval_nochg = 120, + .usb_safety_tmr_h = 4, + .bkup_bat_v = BUP_VCH_SEL_2P6V, + .bkup_bat_i = BUP_ICH_SEL_150UA, + .no_maintenance = false, + .capacity_scaling = false, + .adc_therm = ABx500_ADC_THERM_BATCTRL, + .chg_unknown_bat = false, + .enable_overshoot = false, + .fg_res = 100, + .cap_levels = &cap_levels, + .bat_type = bat_type_thermistor, + .n_btypes = ARRAY_SIZE(bat_type_thermistor), + .batt_id = 0, + .interval_charging = 5, + .interval_not_charging = 120, + .temp_hysteresis = 3, + .gnd_lift_resistance = 34, + .chg_output_curr = ab8500_charge_output_curr_map, + .n_chg_out_curr = ARRAY_SIZE(ab8500_charge_output_curr_map), + .maxi = &ab8500_maxi_params, + .chg_params = &chg, + .fg_params = &fg, + .chg_input_curr = ab8500_charge_input_curr_map, + .n_chg_in_curr = ARRAY_SIZE(ab8500_charge_input_curr_map), +}; + +int ab8500_bm_of_probe(struct device *dev, + struct device_node *np, + struct abx500_bm_data *bm) +{ + const struct batres_vs_temp *tmp_batres_tbl; + struct device_node *battery_node; + const char *btech; + int i; + + /* get phandle to 'battery-info' node */ + battery_node = of_parse_phandle(np, "battery", 0); + if (!battery_node) { + dev_err(dev, "battery node or reference missing\n"); + return -EINVAL; + } + + btech = of_get_property(battery_node, "stericsson,battery-type", NULL); + if (!btech) { + dev_warn(dev, "missing property battery-name/type\n"); + of_node_put(battery_node); + return -EINVAL; + } + + if (strncmp(btech, "LION", 4) == 0) { + bm->no_maintenance = true; + bm->chg_unknown_bat = true; + bm->bat_type[BATTERY_UNKNOWN].charge_full_design = 2600; + bm->bat_type[BATTERY_UNKNOWN].termination_vol = 4150; + bm->bat_type[BATTERY_UNKNOWN].recharge_cap = 95; + bm->bat_type[BATTERY_UNKNOWN].normal_cur_lvl = 520; + bm->bat_type[BATTERY_UNKNOWN].normal_vol_lvl = 4200; + } + + if (of_property_read_bool(battery_node, "thermistor-on-batctrl")) { + if (strncmp(btech, "LION", 4) == 0) + tmp_batres_tbl = temp_to_batres_tbl_9100; + else + tmp_batres_tbl = temp_to_batres_tbl_thermistor; + } else { + bm->n_btypes = 4; + bm->bat_type = bat_type_ext_thermistor; + bm->adc_therm = ABx500_ADC_THERM_BATTEMP; + tmp_batres_tbl = temp_to_batres_tbl_ext_thermistor; + } + + /* select the battery resolution table */ + for (i = 0; i < bm->n_btypes; ++i) + bm->bat_type[i].batres_tbl = tmp_batres_tbl; + + of_node_put(battery_node); + + return 0; +} diff --git a/drivers/power/supply/ab8500_btemp.c b/drivers/power/supply/ab8500_btemp.c new file mode 100644 index 000000000..5a1adceb6 --- /dev/null +++ b/drivers/power/supply/ab8500_btemp.c @@ -0,0 +1,1172 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) ST-Ericsson SA 2012 + * + * Battery temperature driver for AB8500 + * + * Author: + * Johan Palsson <johan.palsson@stericsson.com> + * Karl Komierowski <karl.komierowski@stericsson.com> + * Arun R Murthy <arun.murthy@stericsson.com> + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/completion.h> +#include <linux/workqueue.h> +#include <linux/jiffies.h> +#include <linux/of.h> +#include <linux/mfd/core.h> +#include <linux/mfd/abx500.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500/ab8500-bm.h> +#include <linux/iio/consumer.h> + +#define VTVOUT_V 1800 + +#define BTEMP_THERMAL_LOW_LIMIT -10 +#define BTEMP_THERMAL_MED_LIMIT 0 +#define BTEMP_THERMAL_HIGH_LIMIT_52 52 +#define BTEMP_THERMAL_HIGH_LIMIT_57 57 +#define BTEMP_THERMAL_HIGH_LIMIT_62 62 + +#define BTEMP_BATCTRL_CURR_SRC_7UA 7 +#define BTEMP_BATCTRL_CURR_SRC_20UA 20 + +#define BTEMP_BATCTRL_CURR_SRC_16UA 16 +#define BTEMP_BATCTRL_CURR_SRC_18UA 18 + +#define BTEMP_BATCTRL_CURR_SRC_60UA 60 +#define BTEMP_BATCTRL_CURR_SRC_120UA 120 + +/** + * struct ab8500_btemp_interrupts - ab8500 interrupts + * @name: name of the interrupt + * @isr function pointer to the isr + */ +struct ab8500_btemp_interrupts { + char *name; + irqreturn_t (*isr)(int irq, void *data); +}; + +struct ab8500_btemp_events { + bool batt_rem; + bool btemp_high; + bool btemp_medhigh; + bool btemp_lowmed; + bool btemp_low; + bool ac_conn; + bool usb_conn; +}; + +struct ab8500_btemp_ranges { + int btemp_high_limit; + int btemp_med_limit; + int btemp_low_limit; +}; + +/** + * struct ab8500_btemp - ab8500 BTEMP device information + * @dev: Pointer to the structure device + * @node: List of AB8500 BTEMPs, hence prepared for reentrance + * @curr_source: What current source we use, in uA + * @bat_temp: Dispatched battery temperature in degree Celsius + * @prev_bat_temp Last measured battery temperature in degree Celsius + * @parent: Pointer to the struct ab8500 + * @adc_btemp_ball: ADC channel for the battery ball temperature + * @adc_bat_ctrl: ADC channel for the battery control + * @fg: Pointer to the struct fg + * @bm: Platform specific battery management information + * @btemp_psy: Structure for BTEMP specific battery properties + * @events: Structure for information about events triggered + * @btemp_ranges: Battery temperature range structure + * @btemp_wq: Work queue for measuring the temperature periodically + * @btemp_periodic_work: Work for measuring the temperature periodically + * @initialized: True if battery id read. + */ +struct ab8500_btemp { + struct device *dev; + struct list_head node; + int curr_source; + int bat_temp; + int prev_bat_temp; + struct ab8500 *parent; + struct iio_channel *btemp_ball; + struct iio_channel *bat_ctrl; + struct ab8500_fg *fg; + struct abx500_bm_data *bm; + struct power_supply *btemp_psy; + struct ab8500_btemp_events events; + struct ab8500_btemp_ranges btemp_ranges; + struct workqueue_struct *btemp_wq; + struct delayed_work btemp_periodic_work; + bool initialized; +}; + +/* BTEMP power supply properties */ +static enum power_supply_property ab8500_btemp_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_TEMP, +}; + +static LIST_HEAD(ab8500_btemp_list); + +/** + * ab8500_btemp_get() - returns a reference to the primary AB8500 BTEMP + * (i.e. the first BTEMP in the instance list) + */ +struct ab8500_btemp *ab8500_btemp_get(void) +{ + return list_first_entry(&ab8500_btemp_list, struct ab8500_btemp, node); +} +EXPORT_SYMBOL(ab8500_btemp_get); + +/** + * ab8500_btemp_batctrl_volt_to_res() - convert batctrl voltage to resistance + * @di: pointer to the ab8500_btemp structure + * @v_batctrl: measured batctrl voltage + * @inst_curr: measured instant current + * + * This function returns the battery resistance that is + * derived from the BATCTRL voltage. + * Returns value in Ohms. + */ +static int ab8500_btemp_batctrl_volt_to_res(struct ab8500_btemp *di, + int v_batctrl, int inst_curr) +{ + int rbs; + + if (is_ab8500_1p1_or_earlier(di->parent)) { + /* + * For ABB cut1.0 and 1.1 BAT_CTRL is internally + * connected to 1.8V through a 450k resistor + */ + return (450000 * (v_batctrl)) / (1800 - v_batctrl); + } + + if (di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL) { + /* + * If the battery has internal NTC, we use the current + * source to calculate the resistance. + */ + rbs = (v_batctrl * 1000 + - di->bm->gnd_lift_resistance * inst_curr) + / di->curr_source; + } else { + /* + * BAT_CTRL is internally + * connected to 1.8V through a 80k resistor + */ + rbs = (80000 * (v_batctrl)) / (1800 - v_batctrl); + } + + return rbs; +} + +/** + * ab8500_btemp_read_batctrl_voltage() - measure batctrl voltage + * @di: pointer to the ab8500_btemp structure + * + * This function returns the voltage on BATCTRL. Returns value in mV. + */ +static int ab8500_btemp_read_batctrl_voltage(struct ab8500_btemp *di) +{ + int vbtemp, ret; + static int prev; + + ret = iio_read_channel_processed(di->bat_ctrl, &vbtemp); + if (ret < 0) { + dev_err(di->dev, + "%s ADC conversion failed, using previous value", + __func__); + return prev; + } + prev = vbtemp; + return vbtemp; +} + +/** + * ab8500_btemp_curr_source_enable() - enable/disable batctrl current source + * @di: pointer to the ab8500_btemp structure + * @enable: enable or disable the current source + * + * Enable or disable the current sources for the BatCtrl AD channel + */ +static int ab8500_btemp_curr_source_enable(struct ab8500_btemp *di, + bool enable) +{ + int curr; + int ret = 0; + + /* + * BATCTRL current sources are included on AB8500 cut2.0 + * and future versions + */ + if (is_ab8500_1p1_or_earlier(di->parent)) + return 0; + + /* Only do this for batteries with internal NTC */ + if (di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL && enable) { + + if (di->curr_source == BTEMP_BATCTRL_CURR_SRC_7UA) + curr = BAT_CTRL_7U_ENA; + else + curr = BAT_CTRL_20U_ENA; + + dev_dbg(di->dev, "Set BATCTRL %duA\n", di->curr_source); + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + FORCE_BAT_CTRL_CMP_HIGH, FORCE_BAT_CTRL_CMP_HIGH); + if (ret) { + dev_err(di->dev, "%s failed setting cmp_force\n", + __func__); + return ret; + } + + /* + * We have to wait one 32kHz cycle before enabling + * the current source, since ForceBatCtrlCmpHigh needs + * to be written in a separate cycle + */ + udelay(32); + + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + FORCE_BAT_CTRL_CMP_HIGH | curr); + if (ret) { + dev_err(di->dev, "%s failed enabling current source\n", + __func__); + goto disable_curr_source; + } + } else if (di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL && !enable) { + dev_dbg(di->dev, "Disable BATCTRL curr source\n"); + + /* Write 0 to the curr bits */ + ret = abx500_mask_and_set_register_interruptible( + di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + BAT_CTRL_7U_ENA | BAT_CTRL_20U_ENA, + ~(BAT_CTRL_7U_ENA | BAT_CTRL_20U_ENA)); + + if (ret) { + dev_err(di->dev, "%s failed disabling current source\n", + __func__); + goto disable_curr_source; + } + + /* Enable Pull-Up and comparator */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + BAT_CTRL_PULL_UP_ENA | BAT_CTRL_CMP_ENA, + BAT_CTRL_PULL_UP_ENA | BAT_CTRL_CMP_ENA); + if (ret) { + dev_err(di->dev, "%s failed enabling PU and comp\n", + __func__); + goto enable_pu_comp; + } + + /* + * We have to wait one 32kHz cycle before disabling + * ForceBatCtrlCmpHigh since this needs to be written + * in a separate cycle + */ + udelay(32); + + /* Disable 'force comparator' */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + FORCE_BAT_CTRL_CMP_HIGH, ~FORCE_BAT_CTRL_CMP_HIGH); + if (ret) { + dev_err(di->dev, "%s failed disabling force comp\n", + __func__); + goto disable_force_comp; + } + } + return ret; + + /* + * We have to try unsetting FORCE_BAT_CTRL_CMP_HIGH one more time + * if we got an error above + */ +disable_curr_source: + /* Write 0 to the curr bits */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + BAT_CTRL_7U_ENA | BAT_CTRL_20U_ENA, + ~(BAT_CTRL_7U_ENA | BAT_CTRL_20U_ENA)); + + if (ret) { + dev_err(di->dev, "%s failed disabling current source\n", + __func__); + return ret; + } +enable_pu_comp: + /* Enable Pull-Up and comparator */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + BAT_CTRL_PULL_UP_ENA | BAT_CTRL_CMP_ENA, + BAT_CTRL_PULL_UP_ENA | BAT_CTRL_CMP_ENA); + if (ret) { + dev_err(di->dev, "%s failed enabling PU and comp\n", + __func__); + return ret; + } + +disable_force_comp: + /* + * We have to wait one 32kHz cycle before disabling + * ForceBatCtrlCmpHigh since this needs to be written + * in a separate cycle + */ + udelay(32); + + /* Disable 'force comparator' */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_BAT_CTRL_CURRENT_SOURCE, + FORCE_BAT_CTRL_CMP_HIGH, ~FORCE_BAT_CTRL_CMP_HIGH); + if (ret) { + dev_err(di->dev, "%s failed disabling force comp\n", + __func__); + return ret; + } + + return ret; +} + +/** + * ab8500_btemp_get_batctrl_res() - get battery resistance + * @di: pointer to the ab8500_btemp structure + * + * This function returns the battery pack identification resistance. + * Returns value in Ohms. + */ +static int ab8500_btemp_get_batctrl_res(struct ab8500_btemp *di) +{ + int ret; + int batctrl = 0; + int res; + int inst_curr; + int i; + + /* + * BATCTRL current sources are included on AB8500 cut2.0 + * and future versions + */ + ret = ab8500_btemp_curr_source_enable(di, true); + if (ret) { + dev_err(di->dev, "%s curr source enabled failed\n", __func__); + return ret; + } + + if (!di->fg) + di->fg = ab8500_fg_get(); + if (!di->fg) { + dev_err(di->dev, "No fg found\n"); + return -EINVAL; + } + + ret = ab8500_fg_inst_curr_start(di->fg); + + if (ret) { + dev_err(di->dev, "Failed to start current measurement\n"); + return ret; + } + + do { + msleep(20); + } while (!ab8500_fg_inst_curr_started(di->fg)); + + i = 0; + + do { + batctrl += ab8500_btemp_read_batctrl_voltage(di); + i++; + msleep(20); + } while (!ab8500_fg_inst_curr_done(di->fg)); + batctrl /= i; + + ret = ab8500_fg_inst_curr_finalize(di->fg, &inst_curr); + if (ret) { + dev_err(di->dev, "Failed to finalize current measurement\n"); + return ret; + } + + res = ab8500_btemp_batctrl_volt_to_res(di, batctrl, inst_curr); + + ret = ab8500_btemp_curr_source_enable(di, false); + if (ret) { + dev_err(di->dev, "%s curr source disable failed\n", __func__); + return ret; + } + + dev_dbg(di->dev, "%s batctrl: %d res: %d inst_curr: %d samples: %d\n", + __func__, batctrl, res, inst_curr, i); + + return res; +} + +/** + * ab8500_btemp_res_to_temp() - resistance to temperature + * @di: pointer to the ab8500_btemp structure + * @tbl: pointer to the resiatance to temperature table + * @tbl_size: size of the resistance to temperature table + * @res: resistance to calculate the temperature from + * + * This function returns the battery temperature in degrees Celsius + * based on the NTC resistance. + */ +static int ab8500_btemp_res_to_temp(struct ab8500_btemp *di, + const struct abx500_res_to_temp *tbl, int tbl_size, int res) +{ + int i; + /* + * Calculate the formula for the straight line + * Simple interpolation if we are within + * the resistance table limits, extrapolate + * if resistance is outside the limits. + */ + if (res > tbl[0].resist) + i = 0; + else if (res <= tbl[tbl_size - 1].resist) + i = tbl_size - 2; + else { + i = 0; + while (!(res <= tbl[i].resist && + res > tbl[i + 1].resist)) + i++; + } + + return tbl[i].temp + ((tbl[i + 1].temp - tbl[i].temp) * + (res - tbl[i].resist)) / (tbl[i + 1].resist - tbl[i].resist); +} + +/** + * ab8500_btemp_measure_temp() - measure battery temperature + * @di: pointer to the ab8500_btemp structure + * + * Returns battery temperature (on success) else the previous temperature + */ +static int ab8500_btemp_measure_temp(struct ab8500_btemp *di) +{ + int temp, ret; + static int prev; + int rbat, rntc, vntc; + u8 id; + + id = di->bm->batt_id; + + if (di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL && + id != BATTERY_UNKNOWN) { + + rbat = ab8500_btemp_get_batctrl_res(di); + if (rbat < 0) { + dev_err(di->dev, "%s get batctrl res failed\n", + __func__); + /* + * Return out-of-range temperature so that + * charging is stopped + */ + return BTEMP_THERMAL_LOW_LIMIT; + } + + temp = ab8500_btemp_res_to_temp(di, + di->bm->bat_type[id].r_to_t_tbl, + di->bm->bat_type[id].n_temp_tbl_elements, rbat); + } else { + ret = iio_read_channel_processed(di->btemp_ball, &vntc); + if (ret < 0) { + dev_err(di->dev, + "%s ADC conversion failed," + " using previous value\n", __func__); + return prev; + } + /* + * The PCB NTC is sourced from VTVOUT via a 230kOhm + * resistor. + */ + rntc = 230000 * vntc / (VTVOUT_V - vntc); + + temp = ab8500_btemp_res_to_temp(di, + di->bm->bat_type[id].r_to_t_tbl, + di->bm->bat_type[id].n_temp_tbl_elements, rntc); + prev = temp; + } + dev_dbg(di->dev, "Battery temperature is %d\n", temp); + return temp; +} + +/** + * ab8500_btemp_id() - Identify the connected battery + * @di: pointer to the ab8500_btemp structure + * + * This function will try to identify the battery by reading the ID + * resistor. Some brands use a combined ID resistor with a NTC resistor to + * both be able to identify and to read the temperature of it. + */ +static int ab8500_btemp_id(struct ab8500_btemp *di) +{ + int res; + u8 i; + + di->curr_source = BTEMP_BATCTRL_CURR_SRC_7UA; + di->bm->batt_id = BATTERY_UNKNOWN; + + res = ab8500_btemp_get_batctrl_res(di); + if (res < 0) { + dev_err(di->dev, "%s get batctrl res failed\n", __func__); + return -ENXIO; + } + + /* BATTERY_UNKNOWN is defined on position 0, skip it! */ + for (i = BATTERY_UNKNOWN + 1; i < di->bm->n_btypes; i++) { + if ((res <= di->bm->bat_type[i].resis_high) && + (res >= di->bm->bat_type[i].resis_low)) { + dev_dbg(di->dev, "Battery detected on %s" + " low %d < res %d < high: %d" + " index: %d\n", + di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL ? + "BATCTRL" : "BATTEMP", + di->bm->bat_type[i].resis_low, res, + di->bm->bat_type[i].resis_high, i); + + di->bm->batt_id = i; + break; + } + } + + if (di->bm->batt_id == BATTERY_UNKNOWN) { + dev_warn(di->dev, "Battery identified as unknown" + ", resistance %d Ohm\n", res); + return -ENXIO; + } + + /* + * We only have to change current source if the + * detected type is Type 1. + */ + if (di->bm->adc_therm == ABx500_ADC_THERM_BATCTRL && + di->bm->batt_id == 1) { + dev_dbg(di->dev, "Set BATCTRL current source to 20uA\n"); + di->curr_source = BTEMP_BATCTRL_CURR_SRC_20UA; + } + + return di->bm->batt_id; +} + +/** + * ab8500_btemp_periodic_work() - Measuring the temperature periodically + * @work: pointer to the work_struct structure + * + * Work function for measuring the temperature periodically + */ +static void ab8500_btemp_periodic_work(struct work_struct *work) +{ + int interval; + int bat_temp; + struct ab8500_btemp *di = container_of(work, + struct ab8500_btemp, btemp_periodic_work.work); + + if (!di->initialized) { + /* Identify the battery */ + if (ab8500_btemp_id(di) < 0) + dev_warn(di->dev, "failed to identify the battery\n"); + } + + bat_temp = ab8500_btemp_measure_temp(di); + /* + * Filter battery temperature. + * Allow direct updates on temperature only if two samples result in + * same temperature. Else only allow 1 degree change from previous + * reported value in the direction of the new measurement. + */ + if ((bat_temp == di->prev_bat_temp) || !di->initialized) { + if ((di->bat_temp != di->prev_bat_temp) || !di->initialized) { + di->initialized = true; + di->bat_temp = bat_temp; + power_supply_changed(di->btemp_psy); + } + } else if (bat_temp < di->prev_bat_temp) { + di->bat_temp--; + power_supply_changed(di->btemp_psy); + } else if (bat_temp > di->prev_bat_temp) { + di->bat_temp++; + power_supply_changed(di->btemp_psy); + } + di->prev_bat_temp = bat_temp; + + if (di->events.ac_conn || di->events.usb_conn) + interval = di->bm->temp_interval_chg; + else + interval = di->bm->temp_interval_nochg; + + /* Schedule a new measurement */ + queue_delayed_work(di->btemp_wq, + &di->btemp_periodic_work, + round_jiffies(interval * HZ)); +} + +/** + * ab8500_btemp_batctrlindb_handler() - battery removal detected + * @irq: interrupt number + * @_di: void pointer that has to address of ab8500_btemp + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_btemp_batctrlindb_handler(int irq, void *_di) +{ + struct ab8500_btemp *di = _di; + dev_err(di->dev, "Battery removal detected!\n"); + + di->events.batt_rem = true; + power_supply_changed(di->btemp_psy); + + return IRQ_HANDLED; +} + +/** + * ab8500_btemp_templow_handler() - battery temp lower than 10 degrees + * @irq: interrupt number + * @_di: void pointer that has to address of ab8500_btemp + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_btemp_templow_handler(int irq, void *_di) +{ + struct ab8500_btemp *di = _di; + + if (is_ab8500_3p3_or_earlier(di->parent)) { + dev_dbg(di->dev, "Ignore false btemp low irq" + " for ABB cut 1.0, 1.1, 2.0 and 3.3\n"); + } else { + dev_crit(di->dev, "Battery temperature lower than -10deg c\n"); + + di->events.btemp_low = true; + di->events.btemp_high = false; + di->events.btemp_medhigh = false; + di->events.btemp_lowmed = false; + power_supply_changed(di->btemp_psy); + } + + return IRQ_HANDLED; +} + +/** + * ab8500_btemp_temphigh_handler() - battery temp higher than max temp + * @irq: interrupt number + * @_di: void pointer that has to address of ab8500_btemp + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_btemp_temphigh_handler(int irq, void *_di) +{ + struct ab8500_btemp *di = _di; + + dev_crit(di->dev, "Battery temperature is higher than MAX temp\n"); + + di->events.btemp_high = true; + di->events.btemp_medhigh = false; + di->events.btemp_lowmed = false; + di->events.btemp_low = false; + power_supply_changed(di->btemp_psy); + + return IRQ_HANDLED; +} + +/** + * ab8500_btemp_lowmed_handler() - battery temp between low and medium + * @irq: interrupt number + * @_di: void pointer that has to address of ab8500_btemp + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_btemp_lowmed_handler(int irq, void *_di) +{ + struct ab8500_btemp *di = _di; + + dev_dbg(di->dev, "Battery temperature is between low and medium\n"); + + di->events.btemp_lowmed = true; + di->events.btemp_medhigh = false; + di->events.btemp_high = false; + di->events.btemp_low = false; + power_supply_changed(di->btemp_psy); + + return IRQ_HANDLED; +} + +/** + * ab8500_btemp_medhigh_handler() - battery temp between medium and high + * @irq: interrupt number + * @_di: void pointer that has to address of ab8500_btemp + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_btemp_medhigh_handler(int irq, void *_di) +{ + struct ab8500_btemp *di = _di; + + dev_dbg(di->dev, "Battery temperature is between medium and high\n"); + + di->events.btemp_medhigh = true; + di->events.btemp_lowmed = false; + di->events.btemp_high = false; + di->events.btemp_low = false; + power_supply_changed(di->btemp_psy); + + return IRQ_HANDLED; +} + +/** + * ab8500_btemp_periodic() - Periodic temperature measurements + * @di: pointer to the ab8500_btemp structure + * @enable: enable or disable periodic temperature measurements + * + * Starts of stops periodic temperature measurements. Periodic measurements + * should only be done when a charger is connected. + */ +static void ab8500_btemp_periodic(struct ab8500_btemp *di, + bool enable) +{ + dev_dbg(di->dev, "Enable periodic temperature measurements: %d\n", + enable); + /* + * Make sure a new measurement is done directly by cancelling + * any pending work + */ + cancel_delayed_work_sync(&di->btemp_periodic_work); + + if (enable) + queue_delayed_work(di->btemp_wq, &di->btemp_periodic_work, 0); +} + +/** + * ab8500_btemp_get_temp() - get battery temperature + * @di: pointer to the ab8500_btemp structure + * + * Returns battery temperature + */ +int ab8500_btemp_get_temp(struct ab8500_btemp *di) +{ + int temp = 0; + + /* + * The BTEMP events are not reliabe on AB8500 cut3.3 + * and prior versions + */ + if (is_ab8500_3p3_or_earlier(di->parent)) { + temp = di->bat_temp * 10; + } else { + if (di->events.btemp_low) { + if (temp > di->btemp_ranges.btemp_low_limit) + temp = di->btemp_ranges.btemp_low_limit * 10; + else + temp = di->bat_temp * 10; + } else if (di->events.btemp_high) { + if (temp < di->btemp_ranges.btemp_high_limit) + temp = di->btemp_ranges.btemp_high_limit * 10; + else + temp = di->bat_temp * 10; + } else if (di->events.btemp_lowmed) { + if (temp > di->btemp_ranges.btemp_med_limit) + temp = di->btemp_ranges.btemp_med_limit * 10; + else + temp = di->bat_temp * 10; + } else if (di->events.btemp_medhigh) { + if (temp < di->btemp_ranges.btemp_med_limit) + temp = di->btemp_ranges.btemp_med_limit * 10; + else + temp = di->bat_temp * 10; + } else + temp = di->bat_temp * 10; + } + return temp; +} +EXPORT_SYMBOL(ab8500_btemp_get_temp); + +/** + * ab8500_btemp_get_batctrl_temp() - get the temperature + * @btemp: pointer to the btemp structure + * + * Returns the batctrl temperature in millidegrees + */ +int ab8500_btemp_get_batctrl_temp(struct ab8500_btemp *btemp) +{ + return btemp->bat_temp * 1000; +} +EXPORT_SYMBOL(ab8500_btemp_get_batctrl_temp); + +/** + * ab8500_btemp_get_property() - get the btemp properties + * @psy: pointer to the power_supply structure + * @psp: pointer to the power_supply_property structure + * @val: pointer to the power_supply_propval union + * + * This function gets called when an application tries to get the btemp + * properties by reading the sysfs files. + * online: presence of the battery + * present: presence of the battery + * technology: battery technology + * temp: battery temperature + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_btemp_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ab8500_btemp *di = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + case POWER_SUPPLY_PROP_ONLINE: + if (di->events.batt_rem) + val->intval = 0; + else + val->intval = 1; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = di->bm->bat_type[di->bm->batt_id].name; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = ab8500_btemp_get_temp(di); + break; + default: + return -EINVAL; + } + return 0; +} + +static int ab8500_btemp_get_ext_psy_data(struct device *dev, void *data) +{ + struct power_supply *psy; + struct power_supply *ext = dev_get_drvdata(dev); + const char **supplicants = (const char **)ext->supplied_to; + struct ab8500_btemp *di; + union power_supply_propval ret; + int j; + + psy = (struct power_supply *)data; + di = power_supply_get_drvdata(psy); + + /* + * For all psy where the name of your driver + * appears in any supplied_to + */ + j = match_string(supplicants, ext->num_supplicants, psy->desc->name); + if (j < 0) + return 0; + + /* Go through all properties for the psy */ + for (j = 0; j < ext->desc->num_properties; j++) { + enum power_supply_property prop; + prop = ext->desc->properties[j]; + + if (power_supply_get_property(ext, prop, &ret)) + continue; + + switch (prop) { + case POWER_SUPPLY_PROP_PRESENT: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_MAINS: + /* AC disconnected */ + if (!ret.intval && di->events.ac_conn) { + di->events.ac_conn = false; + } + /* AC connected */ + else if (ret.intval && !di->events.ac_conn) { + di->events.ac_conn = true; + if (!di->events.usb_conn) + ab8500_btemp_periodic(di, true); + } + break; + case POWER_SUPPLY_TYPE_USB: + /* USB disconnected */ + if (!ret.intval && di->events.usb_conn) { + di->events.usb_conn = false; + } + /* USB connected */ + else if (ret.intval && !di->events.usb_conn) { + di->events.usb_conn = true; + if (!di->events.ac_conn) + ab8500_btemp_periodic(di, true); + } + break; + default: + break; + } + break; + default: + break; + } + } + return 0; +} + +/** + * ab8500_btemp_external_power_changed() - callback for power supply changes + * @psy: pointer to the structure power_supply + * + * This function is pointing to the function pointer external_power_changed + * of the structure power_supply. + * This function gets executed when there is a change in the external power + * supply to the btemp. + */ +static void ab8500_btemp_external_power_changed(struct power_supply *psy) +{ + class_for_each_device(power_supply_class, NULL, psy, + ab8500_btemp_get_ext_psy_data); +} + +/* ab8500 btemp driver interrupts and their respective isr */ +static struct ab8500_btemp_interrupts ab8500_btemp_irq[] = { + {"BAT_CTRL_INDB", ab8500_btemp_batctrlindb_handler}, + {"BTEMP_LOW", ab8500_btemp_templow_handler}, + {"BTEMP_HIGH", ab8500_btemp_temphigh_handler}, + {"BTEMP_LOW_MEDIUM", ab8500_btemp_lowmed_handler}, + {"BTEMP_MEDIUM_HIGH", ab8500_btemp_medhigh_handler}, +}; + +#if defined(CONFIG_PM) +static int ab8500_btemp_resume(struct platform_device *pdev) +{ + struct ab8500_btemp *di = platform_get_drvdata(pdev); + + ab8500_btemp_periodic(di, true); + + return 0; +} + +static int ab8500_btemp_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct ab8500_btemp *di = platform_get_drvdata(pdev); + + ab8500_btemp_periodic(di, false); + + return 0; +} +#else +#define ab8500_btemp_suspend NULL +#define ab8500_btemp_resume NULL +#endif + +static int ab8500_btemp_remove(struct platform_device *pdev) +{ + struct ab8500_btemp *di = platform_get_drvdata(pdev); + int i, irq; + + /* Disable interrupts */ + for (i = 0; i < ARRAY_SIZE(ab8500_btemp_irq); i++) { + irq = platform_get_irq_byname(pdev, ab8500_btemp_irq[i].name); + free_irq(irq, di); + } + + /* Delete the work queue */ + destroy_workqueue(di->btemp_wq); + + flush_scheduled_work(); + power_supply_unregister(di->btemp_psy); + + return 0; +} + +static char *supply_interface[] = { + "ab8500_chargalg", + "ab8500_fg", +}; + +static const struct power_supply_desc ab8500_btemp_desc = { + .name = "ab8500_btemp", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = ab8500_btemp_props, + .num_properties = ARRAY_SIZE(ab8500_btemp_props), + .get_property = ab8500_btemp_get_property, + .external_power_changed = ab8500_btemp_external_power_changed, +}; + +static int ab8500_btemp_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct abx500_bm_data *plat = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + struct ab8500_btemp *di; + int irq, i, ret = 0; + u8 val; + + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); + if (!di) { + dev_err(&pdev->dev, "%s no mem for ab8500_btemp\n", __func__); + return -ENOMEM; + } + + if (!plat) { + dev_err(&pdev->dev, "no battery management data supplied\n"); + return -EINVAL; + } + di->bm = plat; + + if (np) { + ret = ab8500_bm_of_probe(&pdev->dev, np, di->bm); + if (ret) { + dev_err(&pdev->dev, "failed to get battery information\n"); + return ret; + } + } + + /* get parent data */ + di->dev = &pdev->dev; + di->parent = dev_get_drvdata(pdev->dev.parent); + + /* Get ADC channels */ + di->btemp_ball = devm_iio_channel_get(&pdev->dev, "btemp_ball"); + if (IS_ERR(di->btemp_ball)) { + if (PTR_ERR(di->btemp_ball) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get BTEMP BALL ADC channel\n"); + return PTR_ERR(di->btemp_ball); + } + di->bat_ctrl = devm_iio_channel_get(&pdev->dev, "bat_ctrl"); + if (IS_ERR(di->bat_ctrl)) { + if (PTR_ERR(di->bat_ctrl) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get BAT CTRL ADC channel\n"); + return PTR_ERR(di->bat_ctrl); + } + + di->initialized = false; + + psy_cfg.supplied_to = supply_interface; + psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); + psy_cfg.drv_data = di; + + /* Create a work queue for the btemp */ + di->btemp_wq = + alloc_workqueue("ab8500_btemp_wq", WQ_MEM_RECLAIM, 0); + if (di->btemp_wq == NULL) { + dev_err(di->dev, "failed to create work queue\n"); + return -ENOMEM; + } + + /* Init work for measuring temperature periodically */ + INIT_DEFERRABLE_WORK(&di->btemp_periodic_work, + ab8500_btemp_periodic_work); + + /* Set BTEMP thermal limits. Low and Med are fixed */ + di->btemp_ranges.btemp_low_limit = BTEMP_THERMAL_LOW_LIMIT; + di->btemp_ranges.btemp_med_limit = BTEMP_THERMAL_MED_LIMIT; + + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_BTEMP_HIGH_TH, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + goto free_btemp_wq; + } + switch (val) { + case BTEMP_HIGH_TH_57_0: + case BTEMP_HIGH_TH_57_1: + di->btemp_ranges.btemp_high_limit = + BTEMP_THERMAL_HIGH_LIMIT_57; + break; + case BTEMP_HIGH_TH_52: + di->btemp_ranges.btemp_high_limit = + BTEMP_THERMAL_HIGH_LIMIT_52; + break; + case BTEMP_HIGH_TH_62: + di->btemp_ranges.btemp_high_limit = + BTEMP_THERMAL_HIGH_LIMIT_62; + break; + } + + /* Register BTEMP power supply class */ + di->btemp_psy = power_supply_register(di->dev, &ab8500_btemp_desc, + &psy_cfg); + if (IS_ERR(di->btemp_psy)) { + dev_err(di->dev, "failed to register BTEMP psy\n"); + ret = PTR_ERR(di->btemp_psy); + goto free_btemp_wq; + } + + /* Register interrupts */ + for (i = 0; i < ARRAY_SIZE(ab8500_btemp_irq); i++) { + irq = platform_get_irq_byname(pdev, ab8500_btemp_irq[i].name); + if (irq < 0) { + ret = irq; + goto free_irq; + } + + ret = request_threaded_irq(irq, NULL, ab8500_btemp_irq[i].isr, + IRQF_SHARED | IRQF_NO_SUSPEND, + ab8500_btemp_irq[i].name, di); + + if (ret) { + dev_err(di->dev, "failed to request %s IRQ %d: %d\n" + , ab8500_btemp_irq[i].name, irq, ret); + goto free_irq; + } + dev_dbg(di->dev, "Requested %s IRQ %d: %d\n", + ab8500_btemp_irq[i].name, irq, ret); + } + + platform_set_drvdata(pdev, di); + + /* Kick off periodic temperature measurements */ + ab8500_btemp_periodic(di, true); + list_add_tail(&di->node, &ab8500_btemp_list); + + return ret; + +free_irq: + /* We also have to free all successfully registered irqs */ + for (i = i - 1; i >= 0; i--) { + irq = platform_get_irq_byname(pdev, ab8500_btemp_irq[i].name); + free_irq(irq, di); + } + + power_supply_unregister(di->btemp_psy); +free_btemp_wq: + destroy_workqueue(di->btemp_wq); + return ret; +} + +static const struct of_device_id ab8500_btemp_match[] = { + { .compatible = "stericsson,ab8500-btemp", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ab8500_btemp_match); + +static struct platform_driver ab8500_btemp_driver = { + .probe = ab8500_btemp_probe, + .remove = ab8500_btemp_remove, + .suspend = ab8500_btemp_suspend, + .resume = ab8500_btemp_resume, + .driver = { + .name = "ab8500-btemp", + .of_match_table = ab8500_btemp_match, + }, +}; + +static int __init ab8500_btemp_init(void) +{ + return platform_driver_register(&ab8500_btemp_driver); +} + +static void __exit ab8500_btemp_exit(void) +{ + platform_driver_unregister(&ab8500_btemp_driver); +} + +device_initcall(ab8500_btemp_init); +module_exit(ab8500_btemp_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Johan Palsson, Karl Komierowski, Arun R Murthy"); +MODULE_ALIAS("platform:ab8500-btemp"); +MODULE_DESCRIPTION("AB8500 battery temperature driver"); diff --git a/drivers/power/supply/ab8500_charger.c b/drivers/power/supply/ab8500_charger.c new file mode 100644 index 000000000..3d627768a --- /dev/null +++ b/drivers/power/supply/ab8500_charger.c @@ -0,0 +1,3711 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) ST-Ericsson SA 2012 + * + * Charger driver for AB8500 + * + * Author: + * Johan Palsson <johan.palsson@stericsson.com> + * Karl Komierowski <karl.komierowski@stericsson.com> + * Arun R Murthy <arun.murthy@stericsson.com> + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/notifier.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/completion.h> +#include <linux/regulator/consumer.h> +#include <linux/err.h> +#include <linux/workqueue.h> +#include <linux/kobject.h> +#include <linux/of.h> +#include <linux/mfd/core.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500.h> +#include <linux/mfd/abx500/ab8500-bm.h> +#include <linux/mfd/abx500/ux500_chargalg.h> +#include <linux/usb/otg.h> +#include <linux/mutex.h> +#include <linux/iio/consumer.h> + +/* Charger constants */ +#define NO_PW_CONN 0 +#define AC_PW_CONN 1 +#define USB_PW_CONN 2 + +#define MAIN_WDOG_ENA 0x01 +#define MAIN_WDOG_KICK 0x02 +#define MAIN_WDOG_DIS 0x00 +#define CHARG_WD_KICK 0x01 +#define MAIN_CH_ENA 0x01 +#define MAIN_CH_NO_OVERSHOOT_ENA_N 0x02 +#define USB_CH_ENA 0x01 +#define USB_CHG_NO_OVERSHOOT_ENA_N 0x02 +#define MAIN_CH_DET 0x01 +#define MAIN_CH_CV_ON 0x04 +#define USB_CH_CV_ON 0x08 +#define VBUS_DET_DBNC100 0x02 +#define VBUS_DET_DBNC1 0x01 +#define OTP_ENABLE_WD 0x01 +#define DROP_COUNT_RESET 0x01 +#define USB_CH_DET 0x01 + +#define MAIN_CH_INPUT_CURR_SHIFT 4 +#define VBUS_IN_CURR_LIM_SHIFT 4 +#define AUTO_VBUS_IN_CURR_LIM_SHIFT 4 +#define VBUS_IN_CURR_LIM_RETRY_SET_TIME 30 /* seconds */ + +#define LED_INDICATOR_PWM_ENA 0x01 +#define LED_INDICATOR_PWM_DIS 0x00 +#define LED_IND_CUR_5MA 0x04 +#define LED_INDICATOR_PWM_DUTY_252_256 0xBF + +/* HW failure constants */ +#define MAIN_CH_TH_PROT 0x02 +#define VBUS_CH_NOK 0x08 +#define USB_CH_TH_PROT 0x02 +#define VBUS_OVV_TH 0x01 +#define MAIN_CH_NOK 0x01 +#define VBUS_DET 0x80 + +#define MAIN_CH_STATUS2_MAINCHGDROP 0x80 +#define MAIN_CH_STATUS2_MAINCHARGERDETDBNC 0x40 +#define USB_CH_VBUSDROP 0x40 +#define USB_CH_VBUSDETDBNC 0x01 + +/* UsbLineStatus register bit masks */ +#define AB8500_USB_LINK_STATUS 0x78 +#define AB8505_USB_LINK_STATUS 0xF8 +#define AB8500_STD_HOST_SUSP 0x18 +#define USB_LINK_STATUS_SHIFT 3 + +/* Watchdog timeout constant */ +#define WD_TIMER 0x30 /* 4min */ +#define WD_KICK_INTERVAL (60 * HZ) + +/* Lowest charger voltage is 3.39V -> 0x4E */ +#define LOW_VOLT_REG 0x4E + +/* Step up/down delay in us */ +#define STEP_UDELAY 1000 + +#define CHARGER_STATUS_POLL 10 /* in ms */ + +#define CHG_WD_INTERVAL (60 * HZ) + +#define AB8500_SW_CONTROL_FALLBACK 0x03 +/* Wait for enumeration before charing in us */ +#define WAIT_ACA_RID_ENUMERATION (5 * 1000) +/*External charger control*/ +#define AB8500_SYS_CHARGER_CONTROL_REG 0x52 +#define EXTERNAL_CHARGER_DISABLE_REG_VAL 0x03 +#define EXTERNAL_CHARGER_ENABLE_REG_VAL 0x07 + +/* UsbLineStatus register - usb types */ +enum ab8500_charger_link_status { + USB_STAT_NOT_CONFIGURED, + USB_STAT_STD_HOST_NC, + USB_STAT_STD_HOST_C_NS, + USB_STAT_STD_HOST_C_S, + USB_STAT_HOST_CHG_NM, + USB_STAT_HOST_CHG_HS, + USB_STAT_HOST_CHG_HS_CHIRP, + USB_STAT_DEDICATED_CHG, + USB_STAT_ACA_RID_A, + USB_STAT_ACA_RID_B, + USB_STAT_ACA_RID_C_NM, + USB_STAT_ACA_RID_C_HS, + USB_STAT_ACA_RID_C_HS_CHIRP, + USB_STAT_HM_IDGND, + USB_STAT_RESERVED, + USB_STAT_NOT_VALID_LINK, + USB_STAT_PHY_EN, + USB_STAT_SUP_NO_IDGND_VBUS, + USB_STAT_SUP_IDGND_VBUS, + USB_STAT_CHARGER_LINE_1, + USB_STAT_CARKIT_1, + USB_STAT_CARKIT_2, + USB_STAT_ACA_DOCK_CHARGER, +}; + +enum ab8500_usb_state { + AB8500_BM_USB_STATE_RESET_HS, /* HighSpeed Reset */ + AB8500_BM_USB_STATE_RESET_FS, /* FullSpeed/LowSpeed Reset */ + AB8500_BM_USB_STATE_CONFIGURED, + AB8500_BM_USB_STATE_SUSPEND, + AB8500_BM_USB_STATE_RESUME, + AB8500_BM_USB_STATE_MAX, +}; + +/* VBUS input current limits supported in AB8500 in mA */ +#define USB_CH_IP_CUR_LVL_0P05 50 +#define USB_CH_IP_CUR_LVL_0P09 98 +#define USB_CH_IP_CUR_LVL_0P19 193 +#define USB_CH_IP_CUR_LVL_0P29 290 +#define USB_CH_IP_CUR_LVL_0P38 380 +#define USB_CH_IP_CUR_LVL_0P45 450 +#define USB_CH_IP_CUR_LVL_0P5 500 +#define USB_CH_IP_CUR_LVL_0P6 600 +#define USB_CH_IP_CUR_LVL_0P7 700 +#define USB_CH_IP_CUR_LVL_0P8 800 +#define USB_CH_IP_CUR_LVL_0P9 900 +#define USB_CH_IP_CUR_LVL_1P0 1000 +#define USB_CH_IP_CUR_LVL_1P1 1100 +#define USB_CH_IP_CUR_LVL_1P3 1300 +#define USB_CH_IP_CUR_LVL_1P4 1400 +#define USB_CH_IP_CUR_LVL_1P5 1500 + +#define VBAT_TRESH_IP_CUR_RED 3800 + +#define to_ab8500_charger_usb_device_info(x) container_of((x), \ + struct ab8500_charger, usb_chg) +#define to_ab8500_charger_ac_device_info(x) container_of((x), \ + struct ab8500_charger, ac_chg) + +/** + * struct ab8500_charger_interrupts - ab8500 interupts + * @name: name of the interrupt + * @isr function pointer to the isr + */ +struct ab8500_charger_interrupts { + char *name; + irqreturn_t (*isr)(int irq, void *data); +}; + +struct ab8500_charger_info { + int charger_connected; + int charger_online; + int charger_voltage; + int cv_active; + bool wd_expired; + int charger_current; +}; + +struct ab8500_charger_event_flags { + bool mainextchnotok; + bool main_thermal_prot; + bool usb_thermal_prot; + bool vbus_ovv; + bool usbchargernotok; + bool chgwdexp; + bool vbus_collapse; + bool vbus_drop_end; +}; + +struct ab8500_charger_usb_state { + int usb_current; + int usb_current_tmp; + enum ab8500_usb_state state; + enum ab8500_usb_state state_tmp; + spinlock_t usb_lock; +}; + +struct ab8500_charger_max_usb_in_curr { + int usb_type_max; + int set_max; + int calculated_max; +}; + +/** + * struct ab8500_charger - ab8500 Charger device information + * @dev: Pointer to the structure device + * @vbus_detected: VBUS detected + * @vbus_detected_start: + * VBUS detected during startup + * @ac_conn: This will be true when the AC charger has been plugged + * @vddadc_en_ac: Indicate if VDD ADC supply is enabled because AC + * charger is enabled + * @vddadc_en_usb: Indicate if VDD ADC supply is enabled because USB + * charger is enabled + * @vbat Battery voltage + * @old_vbat Previously measured battery voltage + * @usb_device_is_unrecognised USB device is unrecognised by the hardware + * @autopower Indicate if we should have automatic pwron after pwrloss + * @autopower_cfg platform specific power config support for "pwron after pwrloss" + * @invalid_charger_detect_state State when forcing AB to use invalid charger + * @is_aca_rid: Incicate if accessory is ACA type + * @current_stepping_sessions: + * Counter for current stepping sessions + * @parent: Pointer to the struct ab8500 + * @adc_main_charger_v ADC channel for main charger voltage + * @adc_main_charger_c ADC channel for main charger current + * @adc_vbus_v ADC channel for USB charger voltage + * @adc_usb_charger_c ADC channel for USB charger current + * @bm: Platform specific battery management information + * @flags: Structure for information about events triggered + * @usb_state: Structure for usb stack information + * @max_usb_in_curr: Max USB charger input current + * @ac_chg: AC charger power supply + * @usb_chg: USB charger power supply + * @ac: Structure that holds the AC charger properties + * @usb: Structure that holds the USB charger properties + * @regu: Pointer to the struct regulator + * @charger_wq: Work queue for the IRQs and checking HW state + * @usb_ipt_crnt_lock: Lock to protect VBUS input current setting from mutuals + * @pm_lock: Lock to prevent system to suspend + * @check_vbat_work Work for checking vbat threshold to adjust vbus current + * @check_hw_failure_work: Work for checking HW state + * @check_usbchgnotok_work: Work for checking USB charger not ok status + * @kick_wd_work: Work for kicking the charger watchdog in case + * of ABB rev 1.* due to the watchog logic bug + * @ac_charger_attached_work: Work for checking if AC charger is still + * connected + * @usb_charger_attached_work: Work for checking if USB charger is still + * connected + * @ac_work: Work for checking AC charger connection + * @detect_usb_type_work: Work for detecting the USB type connected + * @usb_link_status_work: Work for checking the new USB link status + * @usb_state_changed_work: Work for checking USB state + * @attach_work: Work for detecting USB type + * @vbus_drop_end_work: Work for detecting VBUS drop end + * @check_main_thermal_prot_work: + * Work for checking Main thermal status + * @check_usb_thermal_prot_work: + * Work for checking USB thermal status + * @charger_attached_mutex: For controlling the wakelock + */ +struct ab8500_charger { + struct device *dev; + bool vbus_detected; + bool vbus_detected_start; + bool ac_conn; + bool vddadc_en_ac; + bool vddadc_en_usb; + int vbat; + int old_vbat; + bool usb_device_is_unrecognised; + bool autopower; + bool autopower_cfg; + int invalid_charger_detect_state; + int is_aca_rid; + atomic_t current_stepping_sessions; + struct ab8500 *parent; + struct iio_channel *adc_main_charger_v; + struct iio_channel *adc_main_charger_c; + struct iio_channel *adc_vbus_v; + struct iio_channel *adc_usb_charger_c; + struct abx500_bm_data *bm; + struct ab8500_charger_event_flags flags; + struct ab8500_charger_usb_state usb_state; + struct ab8500_charger_max_usb_in_curr max_usb_in_curr; + struct ux500_charger ac_chg; + struct ux500_charger usb_chg; + struct ab8500_charger_info ac; + struct ab8500_charger_info usb; + struct regulator *regu; + struct workqueue_struct *charger_wq; + struct mutex usb_ipt_crnt_lock; + struct delayed_work check_vbat_work; + struct delayed_work check_hw_failure_work; + struct delayed_work check_usbchgnotok_work; + struct delayed_work kick_wd_work; + struct delayed_work usb_state_changed_work; + struct delayed_work attach_work; + struct delayed_work ac_charger_attached_work; + struct delayed_work usb_charger_attached_work; + struct delayed_work vbus_drop_end_work; + struct work_struct ac_work; + struct work_struct detect_usb_type_work; + struct work_struct usb_link_status_work; + struct work_struct check_main_thermal_prot_work; + struct work_struct check_usb_thermal_prot_work; + struct usb_phy *usb_phy; + struct notifier_block nb; + struct mutex charger_attached_mutex; +}; + +/* AC properties */ +static enum power_supply_property ab8500_charger_ac_props[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +/* USB properties */ +static enum power_supply_property ab8500_charger_usb_props[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +/* + * Function for enabling and disabling sw fallback mode + * should always be disabled when no charger is connected. + */ +static void ab8500_enable_disable_sw_fallback(struct ab8500_charger *di, + bool fallback) +{ + u8 val; + u8 reg; + u8 bank; + u8 bit; + int ret; + + dev_dbg(di->dev, "SW Fallback: %d\n", fallback); + + if (is_ab8500(di->parent)) { + bank = 0x15; + reg = 0x0; + bit = 3; + } else { + bank = AB8500_SYS_CTRL1_BLOCK; + reg = AB8500_SW_CONTROL_FALLBACK; + bit = 0; + } + + /* read the register containing fallback bit */ + ret = abx500_get_register_interruptible(di->dev, bank, reg, &val); + if (ret < 0) { + dev_err(di->dev, "%d read failed\n", __LINE__); + return; + } + + if (is_ab8500(di->parent)) { + /* enable the OPT emulation registers */ + ret = abx500_set_register_interruptible(di->dev, 0x11, 0x00, 0x2); + if (ret) { + dev_err(di->dev, "%d write failed\n", __LINE__); + goto disable_otp; + } + } + + if (fallback) + val |= (1 << bit); + else + val &= ~(1 << bit); + + /* write back the changed fallback bit value to register */ + ret = abx500_set_register_interruptible(di->dev, bank, reg, val); + if (ret) { + dev_err(di->dev, "%d write failed\n", __LINE__); + } + +disable_otp: + if (is_ab8500(di->parent)) { + /* disable the set OTP registers again */ + ret = abx500_set_register_interruptible(di->dev, 0x11, 0x00, 0x0); + if (ret) { + dev_err(di->dev, "%d write failed\n", __LINE__); + } + } +} + +/** + * ab8500_power_supply_changed - a wrapper with local extensions for + * power_supply_changed + * @di: pointer to the ab8500_charger structure + * @psy: pointer to power_supply_that have changed. + * + */ +static void ab8500_power_supply_changed(struct ab8500_charger *di, + struct power_supply *psy) +{ + /* + * This happens if we get notifications or interrupts and + * the platform has been configured not to support one or + * other type of charging. + */ + if (!psy) + return; + + if (di->autopower_cfg) { + if (!di->usb.charger_connected && + !di->ac.charger_connected && + di->autopower) { + di->autopower = false; + ab8500_enable_disable_sw_fallback(di, false); + } else if (!di->autopower && + (di->ac.charger_connected || + di->usb.charger_connected)) { + di->autopower = true; + ab8500_enable_disable_sw_fallback(di, true); + } + } + power_supply_changed(psy); +} + +static void ab8500_charger_set_usb_connected(struct ab8500_charger *di, + bool connected) +{ + if (connected != di->usb.charger_connected) { + dev_dbg(di->dev, "USB connected:%i\n", connected); + di->usb.charger_connected = connected; + + if (!connected) + di->flags.vbus_drop_end = false; + + /* + * Sometimes the platform is configured not to support + * USB charging and no psy has been created, but we still + * will get these notifications. + */ + if (di->usb_chg.psy) { + sysfs_notify(&di->usb_chg.psy->dev.kobj, NULL, + "present"); + } + + if (connected) { + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + + if (is_ab8500(di->parent)) + queue_delayed_work(di->charger_wq, + &di->usb_charger_attached_work, + HZ); + } else { + cancel_delayed_work_sync(&di->usb_charger_attached_work); + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + } + } +} + +/** + * ab8500_charger_get_ac_voltage() - get ac charger voltage + * @di: pointer to the ab8500_charger structure + * + * Returns ac charger voltage (on success) + */ +static int ab8500_charger_get_ac_voltage(struct ab8500_charger *di) +{ + int vch, ret; + + /* Only measure voltage if the charger is connected */ + if (di->ac.charger_connected) { + ret = iio_read_channel_processed(di->adc_main_charger_v, &vch); + if (ret < 0) + dev_err(di->dev, "%s ADC conv failed,\n", __func__); + } else { + vch = 0; + } + return vch; +} + +/** + * ab8500_charger_ac_cv() - check if the main charger is in CV mode + * @di: pointer to the ab8500_charger structure + * + * Returns ac charger CV mode (on success) else error code + */ +static int ab8500_charger_ac_cv(struct ab8500_charger *di) +{ + u8 val; + int ret = 0; + + /* Only check CV mode if the charger is online */ + if (di->ac.charger_online) { + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_STATUS1_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return 0; + } + + if (val & MAIN_CH_CV_ON) + ret = 1; + else + ret = 0; + } + + return ret; +} + +/** + * ab8500_charger_get_vbus_voltage() - get vbus voltage + * @di: pointer to the ab8500_charger structure + * + * This function returns the vbus voltage. + * Returns vbus voltage (on success) + */ +static int ab8500_charger_get_vbus_voltage(struct ab8500_charger *di) +{ + int vch, ret; + + /* Only measure voltage if the charger is connected */ + if (di->usb.charger_connected) { + ret = iio_read_channel_processed(di->adc_vbus_v, &vch); + if (ret < 0) + dev_err(di->dev, "%s ADC conv failed,\n", __func__); + } else { + vch = 0; + } + return vch; +} + +/** + * ab8500_charger_get_usb_current() - get usb charger current + * @di: pointer to the ab8500_charger structure + * + * This function returns the usb charger current. + * Returns usb current (on success) and error code on failure + */ +static int ab8500_charger_get_usb_current(struct ab8500_charger *di) +{ + int ich, ret; + + /* Only measure current if the charger is online */ + if (di->usb.charger_online) { + ret = iio_read_channel_processed(di->adc_usb_charger_c, &ich); + if (ret < 0) + dev_err(di->dev, "%s ADC conv failed,\n", __func__); + } else { + ich = 0; + } + return ich; +} + +/** + * ab8500_charger_get_ac_current() - get ac charger current + * @di: pointer to the ab8500_charger structure + * + * This function returns the ac charger current. + * Returns ac current (on success) and error code on failure. + */ +static int ab8500_charger_get_ac_current(struct ab8500_charger *di) +{ + int ich, ret; + + /* Only measure current if the charger is online */ + if (di->ac.charger_online) { + ret = iio_read_channel_processed(di->adc_main_charger_c, &ich); + if (ret < 0) + dev_err(di->dev, "%s ADC conv failed,\n", __func__); + } else { + ich = 0; + } + return ich; +} + +/** + * ab8500_charger_usb_cv() - check if the usb charger is in CV mode + * @di: pointer to the ab8500_charger structure + * + * Returns ac charger CV mode (on success) else error code + */ +static int ab8500_charger_usb_cv(struct ab8500_charger *di) +{ + int ret; + u8 val; + + /* Only check CV mode if the charger is online */ + if (di->usb.charger_online) { + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_USBCH_STAT1_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return 0; + } + + if (val & USB_CH_CV_ON) + ret = 1; + else + ret = 0; + } else { + ret = 0; + } + + return ret; +} + +/** + * ab8500_charger_detect_chargers() - Detect the connected chargers + * @di: pointer to the ab8500_charger structure + * @probe: if probe, don't delay and wait for HW + * + * Returns the type of charger connected. + * For USB it will not mean we can actually charge from it + * but that there is a USB cable connected that we have to + * identify. This is used during startup when we don't get + * interrupts of the charger detection + * + * Returns an integer value, that means, + * NO_PW_CONN no power supply is connected + * AC_PW_CONN if the AC power supply is connected + * USB_PW_CONN if the USB power supply is connected + * AC_PW_CONN + USB_PW_CONN if USB and AC power supplies are both connected + */ +static int ab8500_charger_detect_chargers(struct ab8500_charger *di, bool probe) +{ + int result = NO_PW_CONN; + int ret; + u8 val; + + /* Check for AC charger */ + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_STATUS1_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + + if (val & MAIN_CH_DET) + result = AC_PW_CONN; + + /* Check for USB charger */ + + if (!probe) { + /* + * AB8500 says VBUS_DET_DBNC1 & VBUS_DET_DBNC100 + * when disconnecting ACA even though no + * charger was connected. Try waiting a little + * longer than the 100 ms of VBUS_DET_DBNC100... + */ + msleep(110); + } + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_USBCH_STAT1_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + dev_dbg(di->dev, + "%s AB8500_CH_USBCH_STAT1_REG %x\n", __func__, + val); + if ((val & VBUS_DET_DBNC1) && (val & VBUS_DET_DBNC100)) + result |= USB_PW_CONN; + + return result; +} + +/** + * ab8500_charger_max_usb_curr() - get the max curr for the USB type + * @di: pointer to the ab8500_charger structure + * @link_status: the identified USB type + * + * Get the maximum current that is allowed to be drawn from the host + * based on the USB type. + * Returns error code in case of failure else 0 on success + */ +static int ab8500_charger_max_usb_curr(struct ab8500_charger *di, + enum ab8500_charger_link_status link_status) +{ + int ret = 0; + + di->usb_device_is_unrecognised = false; + + /* + * Platform only supports USB 2.0. + * This means that charging current from USB source + * is maximum 500 mA. Every occurrence of USB_STAT_*_HOST_* + * should set USB_CH_IP_CUR_LVL_0P5. + */ + + switch (link_status) { + case USB_STAT_STD_HOST_NC: + case USB_STAT_STD_HOST_C_NS: + case USB_STAT_STD_HOST_C_S: + dev_dbg(di->dev, "USB Type - Standard host is " + "detected through USB driver\n"); + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + di->is_aca_rid = 0; + break; + case USB_STAT_HOST_CHG_HS_CHIRP: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + di->is_aca_rid = 0; + break; + case USB_STAT_HOST_CHG_HS: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + di->is_aca_rid = 0; + break; + case USB_STAT_ACA_RID_C_HS: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P9; + di->is_aca_rid = 0; + break; + case USB_STAT_ACA_RID_A: + /* + * Dedicated charger level minus maximum current accessory + * can consume (900mA). Closest level is 500mA + */ + dev_dbg(di->dev, "USB_STAT_ACA_RID_A detected\n"); + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + di->is_aca_rid = 1; + break; + case USB_STAT_ACA_RID_B: + /* + * Dedicated charger level minus 120mA (20mA for ACA and + * 100mA for potential accessory). Closest level is 1300mA + */ + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_1P3; + dev_dbg(di->dev, "USB Type - 0x%02x MaxCurr: %d", link_status, + di->max_usb_in_curr.usb_type_max); + di->is_aca_rid = 1; + break; + case USB_STAT_HOST_CHG_NM: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + di->is_aca_rid = 0; + break; + case USB_STAT_DEDICATED_CHG: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_1P5; + di->is_aca_rid = 0; + break; + case USB_STAT_ACA_RID_C_HS_CHIRP: + case USB_STAT_ACA_RID_C_NM: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_1P5; + di->is_aca_rid = 1; + break; + case USB_STAT_NOT_CONFIGURED: + if (di->vbus_detected) { + di->usb_device_is_unrecognised = true; + dev_dbg(di->dev, "USB Type - Legacy charger.\n"); + di->max_usb_in_curr.usb_type_max = + USB_CH_IP_CUR_LVL_1P5; + break; + } + fallthrough; + case USB_STAT_HM_IDGND: + dev_err(di->dev, "USB Type - Charging not allowed\n"); + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P05; + ret = -ENXIO; + break; + case USB_STAT_RESERVED: + if (is_ab8500(di->parent)) { + di->flags.vbus_collapse = true; + dev_err(di->dev, "USB Type - USB_STAT_RESERVED " + "VBUS has collapsed\n"); + ret = -ENXIO; + break; + } else { + dev_dbg(di->dev, "USB Type - Charging not allowed\n"); + di->max_usb_in_curr.usb_type_max = + USB_CH_IP_CUR_LVL_0P05; + dev_dbg(di->dev, "USB Type - 0x%02x MaxCurr: %d", + link_status, + di->max_usb_in_curr.usb_type_max); + ret = -ENXIO; + break; + } + case USB_STAT_CARKIT_1: + case USB_STAT_CARKIT_2: + case USB_STAT_ACA_DOCK_CHARGER: + case USB_STAT_CHARGER_LINE_1: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + dev_dbg(di->dev, "USB Type - 0x%02x MaxCurr: %d", link_status, + di->max_usb_in_curr.usb_type_max); + break; + case USB_STAT_NOT_VALID_LINK: + dev_err(di->dev, "USB Type invalid - try charging anyway\n"); + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + break; + + default: + dev_err(di->dev, "USB Type - Unknown\n"); + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P05; + ret = -ENXIO; + break; + } + + di->max_usb_in_curr.set_max = di->max_usb_in_curr.usb_type_max; + dev_dbg(di->dev, "USB Type - 0x%02x MaxCurr: %d", + link_status, di->max_usb_in_curr.set_max); + + return ret; +} + +/** + * ab8500_charger_read_usb_type() - read the type of usb connected + * @di: pointer to the ab8500_charger structure + * + * Detect the type of the plugged USB + * Returns error code in case of failure else 0 on success + */ +static int ab8500_charger_read_usb_type(struct ab8500_charger *di) +{ + int ret; + u8 val; + + ret = abx500_get_register_interruptible(di->dev, + AB8500_INTERRUPT, AB8500_IT_SOURCE21_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + if (is_ab8500(di->parent)) + ret = abx500_get_register_interruptible(di->dev, AB8500_USB, + AB8500_USB_LINE_STAT_REG, &val); + else + ret = abx500_get_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINK1_STAT_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + + /* get the USB type */ + if (is_ab8500(di->parent)) + val = (val & AB8500_USB_LINK_STATUS) >> USB_LINK_STATUS_SHIFT; + else + val = (val & AB8505_USB_LINK_STATUS) >> USB_LINK_STATUS_SHIFT; + ret = ab8500_charger_max_usb_curr(di, + (enum ab8500_charger_link_status) val); + + return ret; +} + +/** + * ab8500_charger_detect_usb_type() - get the type of usb connected + * @di: pointer to the ab8500_charger structure + * + * Detect the type of the plugged USB + * Returns error code in case of failure else 0 on success + */ +static int ab8500_charger_detect_usb_type(struct ab8500_charger *di) +{ + int i, ret; + u8 val; + + /* + * On getting the VBUS rising edge detect interrupt there + * is a 250ms delay after which the register UsbLineStatus + * is filled with valid data. + */ + for (i = 0; i < 10; i++) { + msleep(250); + ret = abx500_get_register_interruptible(di->dev, + AB8500_INTERRUPT, AB8500_IT_SOURCE21_REG, + &val); + dev_dbg(di->dev, "%s AB8500_IT_SOURCE21_REG %x\n", + __func__, val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + + if (is_ab8500(di->parent)) + ret = abx500_get_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINE_STAT_REG, &val); + else + ret = abx500_get_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINK1_STAT_REG, &val); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return ret; + } + dev_dbg(di->dev, "%s AB8500_USB_LINE_STAT_REG %x\n", __func__, + val); + /* + * Until the IT source register is read the UsbLineStatus + * register is not updated, hence doing the same + * Revisit this: + */ + + /* get the USB type */ + if (is_ab8500(di->parent)) + val = (val & AB8500_USB_LINK_STATUS) >> + USB_LINK_STATUS_SHIFT; + else + val = (val & AB8505_USB_LINK_STATUS) >> + USB_LINK_STATUS_SHIFT; + if (val) + break; + } + ret = ab8500_charger_max_usb_curr(di, + (enum ab8500_charger_link_status) val); + + return ret; +} + +/* + * This array maps the raw hex value to charger voltage used by the AB8500 + * Values taken from the UM0836 + */ +static int ab8500_charger_voltage_map[] = { + 3500 , + 3525 , + 3550 , + 3575 , + 3600 , + 3625 , + 3650 , + 3675 , + 3700 , + 3725 , + 3750 , + 3775 , + 3800 , + 3825 , + 3850 , + 3875 , + 3900 , + 3925 , + 3950 , + 3975 , + 4000 , + 4025 , + 4050 , + 4060 , + 4070 , + 4080 , + 4090 , + 4100 , + 4110 , + 4120 , + 4130 , + 4140 , + 4150 , + 4160 , + 4170 , + 4180 , + 4190 , + 4200 , + 4210 , + 4220 , + 4230 , + 4240 , + 4250 , + 4260 , + 4270 , + 4280 , + 4290 , + 4300 , + 4310 , + 4320 , + 4330 , + 4340 , + 4350 , + 4360 , + 4370 , + 4380 , + 4390 , + 4400 , + 4410 , + 4420 , + 4430 , + 4440 , + 4450 , + 4460 , + 4470 , + 4480 , + 4490 , + 4500 , + 4510 , + 4520 , + 4530 , + 4540 , + 4550 , + 4560 , + 4570 , + 4580 , + 4590 , + 4600 , +}; + +static int ab8500_voltage_to_regval(int voltage) +{ + int i; + + /* Special case for voltage below 3.5V */ + if (voltage < ab8500_charger_voltage_map[0]) + return LOW_VOLT_REG; + + for (i = 1; i < ARRAY_SIZE(ab8500_charger_voltage_map); i++) { + if (voltage < ab8500_charger_voltage_map[i]) + return i - 1; + } + + /* If not last element, return error */ + i = ARRAY_SIZE(ab8500_charger_voltage_map) - 1; + if (voltage == ab8500_charger_voltage_map[i]) + return i; + else + return -1; +} + +static int ab8500_current_to_regval(struct ab8500_charger *di, int curr) +{ + int i; + + if (curr < di->bm->chg_output_curr[0]) + return 0; + + for (i = 0; i < di->bm->n_chg_out_curr; i++) { + if (curr < di->bm->chg_output_curr[i]) + return i - 1; + } + + /* If not last element, return error */ + i = di->bm->n_chg_out_curr - 1; + if (curr == di->bm->chg_output_curr[i]) + return i; + else + return -1; +} + +static int ab8500_vbus_in_curr_to_regval(struct ab8500_charger *di, int curr) +{ + int i; + + if (curr < di->bm->chg_input_curr[0]) + return 0; + + for (i = 0; i < di->bm->n_chg_in_curr; i++) { + if (curr < di->bm->chg_input_curr[i]) + return i - 1; + } + + /* If not last element, return error */ + i = di->bm->n_chg_in_curr - 1; + if (curr == di->bm->chg_input_curr[i]) + return i; + else + return -1; +} + +/** + * ab8500_charger_get_usb_cur() - get usb current + * @di: pointer to the ab8500_charger structre + * + * The usb stack provides the maximum current that can be drawn from + * the standard usb host. This will be in mA. + * This function converts current in mA to a value that can be written + * to the register. Returns -1 if charging is not allowed + */ +static int ab8500_charger_get_usb_cur(struct ab8500_charger *di) +{ + int ret = 0; + switch (di->usb_state.usb_current) { + case 100: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P09; + break; + case 200: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P19; + break; + case 300: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P29; + break; + case 400: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P38; + break; + case 500: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P5; + break; + default: + di->max_usb_in_curr.usb_type_max = USB_CH_IP_CUR_LVL_0P05; + ret = -EPERM; + break; + } + di->max_usb_in_curr.set_max = di->max_usb_in_curr.usb_type_max; + return ret; +} + +/** + * ab8500_charger_check_continue_stepping() - Check to allow stepping + * @di: pointer to the ab8500_charger structure + * @reg: select what charger register to check + * + * Check if current stepping should be allowed to continue. + * Checks if charger source has not collapsed. If it has, further stepping + * is not allowed. + */ +static bool ab8500_charger_check_continue_stepping(struct ab8500_charger *di, + int reg) +{ + if (reg == AB8500_USBCH_IPT_CRNTLVL_REG) + return !di->flags.vbus_drop_end; + else + return true; +} + +/** + * ab8500_charger_set_current() - set charger current + * @di: pointer to the ab8500_charger structure + * @ich: charger current, in mA + * @reg: select what charger register to set + * + * Set charger current. + * There is no state machine in the AB to step up/down the charger + * current to avoid dips and spikes on MAIN, VBUS and VBAT when + * charging is started. Instead we need to implement + * this charger current step-up/down here. + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_set_current(struct ab8500_charger *di, + int ich, int reg) +{ + int ret = 0; + int curr_index, prev_curr_index, shift_value, i; + u8 reg_value; + u32 step_udelay; + bool no_stepping = false; + + atomic_inc(&di->current_stepping_sessions); + + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + reg, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s read failed\n", __func__); + goto exit_set_current; + } + + switch (reg) { + case AB8500_MCH_IPT_CURLVL_REG: + shift_value = MAIN_CH_INPUT_CURR_SHIFT; + prev_curr_index = (reg_value >> shift_value); + curr_index = ab8500_current_to_regval(di, ich); + step_udelay = STEP_UDELAY; + if (!di->ac.charger_connected) + no_stepping = true; + break; + case AB8500_USBCH_IPT_CRNTLVL_REG: + shift_value = VBUS_IN_CURR_LIM_SHIFT; + prev_curr_index = (reg_value >> shift_value); + curr_index = ab8500_vbus_in_curr_to_regval(di, ich); + step_udelay = STEP_UDELAY * 100; + + if (!di->usb.charger_connected) + no_stepping = true; + break; + case AB8500_CH_OPT_CRNTLVL_REG: + shift_value = 0; + prev_curr_index = (reg_value >> shift_value); + curr_index = ab8500_current_to_regval(di, ich); + step_udelay = STEP_UDELAY; + if (curr_index && (curr_index - prev_curr_index) > 1) + step_udelay *= 100; + + if (!di->usb.charger_connected && !di->ac.charger_connected) + no_stepping = true; + + break; + default: + dev_err(di->dev, "%s current register not valid\n", __func__); + ret = -ENXIO; + goto exit_set_current; + } + + if (curr_index < 0) { + dev_err(di->dev, "requested current limit out-of-range\n"); + ret = -ENXIO; + goto exit_set_current; + } + + /* only update current if it's been changed */ + if (prev_curr_index == curr_index) { + dev_dbg(di->dev, "%s current not changed for reg: 0x%02x\n", + __func__, reg); + ret = 0; + goto exit_set_current; + } + + dev_dbg(di->dev, "%s set charger current: %d mA for reg: 0x%02x\n", + __func__, ich, reg); + + if (no_stepping) { + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + reg, (u8)curr_index << shift_value); + if (ret) + dev_err(di->dev, "%s write failed\n", __func__); + } else if (prev_curr_index > curr_index) { + for (i = prev_curr_index - 1; i >= curr_index; i--) { + dev_dbg(di->dev, "curr change_1 to: %x for 0x%02x\n", + (u8) i << shift_value, reg); + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, reg, (u8)i << shift_value); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + goto exit_set_current; + } + if (i != curr_index) + usleep_range(step_udelay, step_udelay * 2); + } + } else { + bool allow = true; + for (i = prev_curr_index + 1; i <= curr_index && allow; i++) { + dev_dbg(di->dev, "curr change_2 to: %x for 0x%02x\n", + (u8)i << shift_value, reg); + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, reg, (u8)i << shift_value); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + goto exit_set_current; + } + if (i != curr_index) + usleep_range(step_udelay, step_udelay * 2); + + allow = ab8500_charger_check_continue_stepping(di, reg); + } + } + +exit_set_current: + atomic_dec(&di->current_stepping_sessions); + + return ret; +} + +/** + * ab8500_charger_set_vbus_in_curr() - set VBUS input current limit + * @di: pointer to the ab8500_charger structure + * @ich_in: charger input current limit + * + * Sets the current that can be drawn from the USB host + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_set_vbus_in_curr(struct ab8500_charger *di, + int ich_in) +{ + int min_value; + int ret; + + /* We should always use to lowest current limit */ + min_value = min(di->bm->chg_params->usb_curr_max, ich_in); + if (di->max_usb_in_curr.set_max > 0) + min_value = min(di->max_usb_in_curr.set_max, min_value); + + if (di->usb_state.usb_current >= 0) + min_value = min(di->usb_state.usb_current, min_value); + + switch (min_value) { + case 100: + if (di->vbat < VBAT_TRESH_IP_CUR_RED) + min_value = USB_CH_IP_CUR_LVL_0P05; + break; + case 500: + if (di->vbat < VBAT_TRESH_IP_CUR_RED) + min_value = USB_CH_IP_CUR_LVL_0P45; + break; + default: + break; + } + + dev_info(di->dev, "VBUS input current limit set to %d mA\n", min_value); + + mutex_lock(&di->usb_ipt_crnt_lock); + ret = ab8500_charger_set_current(di, min_value, + AB8500_USBCH_IPT_CRNTLVL_REG); + mutex_unlock(&di->usb_ipt_crnt_lock); + + return ret; +} + +/** + * ab8500_charger_set_main_in_curr() - set main charger input current + * @di: pointer to the ab8500_charger structure + * @ich_in: input charger current, in mA + * + * Set main charger input current. + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_set_main_in_curr(struct ab8500_charger *di, + int ich_in) +{ + return ab8500_charger_set_current(di, ich_in, + AB8500_MCH_IPT_CURLVL_REG); +} + +/** + * ab8500_charger_set_output_curr() - set charger output current + * @di: pointer to the ab8500_charger structure + * @ich_out: output charger current, in mA + * + * Set charger output current. + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_set_output_curr(struct ab8500_charger *di, + int ich_out) +{ + return ab8500_charger_set_current(di, ich_out, + AB8500_CH_OPT_CRNTLVL_REG); +} + +/** + * ab8500_charger_led_en() - turn on/off chargign led + * @di: pointer to the ab8500_charger structure + * @on: flag to turn on/off the chargign led + * + * Power ON/OFF charging LED indication + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_led_en(struct ab8500_charger *di, int on) +{ + int ret; + + if (on) { + /* Power ON charging LED indicator, set LED current to 5mA */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_LED_INDICATOR_PWM_CTRL, + (LED_IND_CUR_5MA | LED_INDICATOR_PWM_ENA)); + if (ret) { + dev_err(di->dev, "Power ON LED failed\n"); + return ret; + } + /* LED indicator PWM duty cycle 252/256 */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_LED_INDICATOR_PWM_DUTY, + LED_INDICATOR_PWM_DUTY_252_256); + if (ret) { + dev_err(di->dev, "Set LED PWM duty cycle failed\n"); + return ret; + } + } else { + /* Power off charging LED indicator */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_LED_INDICATOR_PWM_CTRL, + LED_INDICATOR_PWM_DIS); + if (ret) { + dev_err(di->dev, "Power-off LED failed\n"); + return ret; + } + } + + return ret; +} + +/** + * ab8500_charger_ac_en() - enable or disable ac charging + * @di: pointer to the ab8500_charger structure + * @enable: enable/disable flag + * @vset: charging voltage + * @iset: charging current + * + * Enable/Disable AC/Mains charging and turns on/off the charging led + * respectively. + **/ +static int ab8500_charger_ac_en(struct ux500_charger *charger, + int enable, int vset, int iset) +{ + int ret; + int volt_index; + int curr_index; + int input_curr_index; + u8 overshoot = 0; + + struct ab8500_charger *di = to_ab8500_charger_ac_device_info(charger); + + if (enable) { + /* Check if AC is connected */ + if (!di->ac.charger_connected) { + dev_err(di->dev, "AC charger not connected\n"); + return -ENXIO; + } + + /* Enable AC charging */ + dev_dbg(di->dev, "Enable AC: %dmV %dmA\n", vset, iset); + + /* + * Due to a bug in AB8500, BTEMP_HIGH/LOW interrupts + * will be triggered every time we enable the VDD ADC supply. + * This will turn off charging for a short while. + * It can be avoided by having the supply on when + * there is a charger enabled. Normally the VDD ADC supply + * is enabled every time a GPADC conversion is triggered. + * We will force it to be enabled from this driver to have + * the GPADC module independent of the AB8500 chargers + */ + if (!di->vddadc_en_ac) { + ret = regulator_enable(di->regu); + if (ret) + dev_warn(di->dev, + "Failed to enable regulator\n"); + else + di->vddadc_en_ac = true; + } + + /* Check if the requested voltage or current is valid */ + volt_index = ab8500_voltage_to_regval(vset); + curr_index = ab8500_current_to_regval(di, iset); + input_curr_index = ab8500_current_to_regval(di, + di->bm->chg_params->ac_curr_max); + if (volt_index < 0 || curr_index < 0 || input_curr_index < 0) { + dev_err(di->dev, + "Charger voltage or current too high, " + "charging not started\n"); + return -ENXIO; + } + + /* ChVoltLevel: maximum battery charging voltage */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_VOLT_LVL_REG, (u8) volt_index); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + return ret; + } + /* MainChInputCurr: current that can be drawn from the charger*/ + ret = ab8500_charger_set_main_in_curr(di, + di->bm->chg_params->ac_curr_max); + if (ret) { + dev_err(di->dev, "%s Failed to set MainChInputCurr\n", + __func__); + return ret; + } + /* ChOutputCurentLevel: protected output current */ + ret = ab8500_charger_set_output_curr(di, iset); + if (ret) { + dev_err(di->dev, "%s " + "Failed to set ChOutputCurentLevel\n", + __func__); + return ret; + } + + /* Check if VBAT overshoot control should be enabled */ + if (!di->bm->enable_overshoot) + overshoot = MAIN_CH_NO_OVERSHOOT_ENA_N; + + /* Enable Main Charger */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_MCH_CTRL1, MAIN_CH_ENA | overshoot); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + return ret; + } + + /* Power on charging LED indication */ + ret = ab8500_charger_led_en(di, true); + if (ret < 0) + dev_err(di->dev, "failed to enable LED\n"); + + di->ac.charger_online = 1; + } else { + /* Disable AC charging */ + if (is_ab8500_1p1_or_earlier(di->parent)) { + /* + * For ABB revision 1.0 and 1.1 there is a bug in the + * watchdog logic. That means we have to continuously + * kick the charger watchdog even when no charger is + * connected. This is only valid once the AC charger + * has been enabled. This is a bug that is not handled + * by the algorithm and the watchdog have to be kicked + * by the charger driver when the AC charger + * is disabled + */ + if (di->ac_conn) { + queue_delayed_work(di->charger_wq, + &di->kick_wd_work, + round_jiffies(WD_KICK_INTERVAL)); + } + + /* + * We can't turn off charging completely + * due to a bug in AB8500 cut1. + * If we do, charging will not start again. + * That is why we set the lowest voltage + * and current possible + */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_CH_VOLT_LVL_REG, CH_VOL_LVL_3P5); + if (ret) { + dev_err(di->dev, + "%s write failed\n", __func__); + return ret; + } + + ret = ab8500_charger_set_output_curr(di, 0); + if (ret) { + dev_err(di->dev, "%s " + "Failed to set ChOutputCurentLevel\n", + __func__); + return ret; + } + } else { + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_MCH_CTRL1, 0); + if (ret) { + dev_err(di->dev, + "%s write failed\n", __func__); + return ret; + } + } + + ret = ab8500_charger_led_en(di, false); + if (ret < 0) + dev_err(di->dev, "failed to disable LED\n"); + + di->ac.charger_online = 0; + di->ac.wd_expired = false; + + /* Disable regulator if enabled */ + if (di->vddadc_en_ac) { + regulator_disable(di->regu); + di->vddadc_en_ac = false; + } + + dev_dbg(di->dev, "%s Disabled AC charging\n", __func__); + } + ab8500_power_supply_changed(di, di->ac_chg.psy); + + return ret; +} + +/** + * ab8500_charger_usb_en() - enable usb charging + * @di: pointer to the ab8500_charger structure + * @enable: enable/disable flag + * @vset: charging voltage + * @ich_out: charger output current + * + * Enable/Disable USB charging and turns on/off the charging led respectively. + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_usb_en(struct ux500_charger *charger, + int enable, int vset, int ich_out) +{ + int ret; + int volt_index; + int curr_index; + u8 overshoot = 0; + + struct ab8500_charger *di = to_ab8500_charger_usb_device_info(charger); + + if (enable) { + /* Check if USB is connected */ + if (!di->usb.charger_connected) { + dev_err(di->dev, "USB charger not connected\n"); + return -ENXIO; + } + + /* + * Due to a bug in AB8500, BTEMP_HIGH/LOW interrupts + * will be triggered every time we enable the VDD ADC supply. + * This will turn off charging for a short while. + * It can be avoided by having the supply on when + * there is a charger enabled. Normally the VDD ADC supply + * is enabled every time a GPADC conversion is triggered. + * We will force it to be enabled from this driver to have + * the GPADC module independent of the AB8500 chargers + */ + if (!di->vddadc_en_usb) { + ret = regulator_enable(di->regu); + if (ret) + dev_warn(di->dev, + "Failed to enable regulator\n"); + else + di->vddadc_en_usb = true; + } + + /* Enable USB charging */ + dev_dbg(di->dev, "Enable USB: %dmV %dmA\n", vset, ich_out); + + /* Check if the requested voltage or current is valid */ + volt_index = ab8500_voltage_to_regval(vset); + curr_index = ab8500_current_to_regval(di, ich_out); + if (volt_index < 0 || curr_index < 0) { + dev_err(di->dev, + "Charger voltage or current too high, " + "charging not started\n"); + return -ENXIO; + } + + /* + * ChVoltLevel: max voltage up to which battery can be + * charged + */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_VOLT_LVL_REG, (u8) volt_index); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + return ret; + } + /* Check if VBAT overshoot control should be enabled */ + if (!di->bm->enable_overshoot) + overshoot = USB_CHG_NO_OVERSHOOT_ENA_N; + + /* Enable USB Charger */ + dev_dbg(di->dev, + "Enabling USB with write to AB8500_USBCH_CTRL1_REG\n"); + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_USBCH_CTRL1_REG, USB_CH_ENA | overshoot); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + return ret; + } + + /* If success power on charging LED indication */ + ret = ab8500_charger_led_en(di, true); + if (ret < 0) + dev_err(di->dev, "failed to enable LED\n"); + + di->usb.charger_online = 1; + + /* USBChInputCurr: current that can be drawn from the usb */ + ret = ab8500_charger_set_vbus_in_curr(di, + di->max_usb_in_curr.usb_type_max); + if (ret) { + dev_err(di->dev, "setting USBChInputCurr failed\n"); + return ret; + } + + /* ChOutputCurentLevel: protected output current */ + ret = ab8500_charger_set_output_curr(di, ich_out); + if (ret) { + dev_err(di->dev, "%s " + "Failed to set ChOutputCurentLevel\n", + __func__); + return ret; + } + + queue_delayed_work(di->charger_wq, &di->check_vbat_work, HZ); + + } else { + /* Disable USB charging */ + dev_dbg(di->dev, "%s Disabled USB charging\n", __func__); + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_USBCH_CTRL1_REG, 0); + if (ret) { + dev_err(di->dev, + "%s write failed\n", __func__); + return ret; + } + + ret = ab8500_charger_led_en(di, false); + if (ret < 0) + dev_err(di->dev, "failed to disable LED\n"); + /* USBChInputCurr: current that can be drawn from the usb */ + ret = ab8500_charger_set_vbus_in_curr(di, 0); + if (ret) { + dev_err(di->dev, "setting USBChInputCurr failed\n"); + return ret; + } + + /* ChOutputCurentLevel: protected output current */ + ret = ab8500_charger_set_output_curr(di, 0); + if (ret) { + dev_err(di->dev, "%s " + "Failed to reset ChOutputCurentLevel\n", + __func__); + return ret; + } + di->usb.charger_online = 0; + di->usb.wd_expired = false; + + /* Disable regulator if enabled */ + if (di->vddadc_en_usb) { + regulator_disable(di->regu); + di->vddadc_en_usb = false; + } + + dev_dbg(di->dev, "%s Disabled USB charging\n", __func__); + + /* Cancel any pending Vbat check work */ + cancel_delayed_work(&di->check_vbat_work); + + } + ab8500_power_supply_changed(di, di->usb_chg.psy); + + return ret; +} + +static int ab8500_external_charger_prepare(struct notifier_block *charger_nb, + unsigned long event, void *data) +{ + int ret; + struct device *dev = data; + /*Toggle External charger control pin*/ + ret = abx500_set_register_interruptible(dev, AB8500_SYS_CTRL1_BLOCK, + AB8500_SYS_CHARGER_CONTROL_REG, + EXTERNAL_CHARGER_DISABLE_REG_VAL); + if (ret < 0) { + dev_err(dev, "write reg failed %d\n", ret); + goto out; + } + ret = abx500_set_register_interruptible(dev, AB8500_SYS_CTRL1_BLOCK, + AB8500_SYS_CHARGER_CONTROL_REG, + EXTERNAL_CHARGER_ENABLE_REG_VAL); + if (ret < 0) + dev_err(dev, "Write reg failed %d\n", ret); + +out: + return ret; +} + +/** + * ab8500_charger_usb_check_enable() - enable usb charging + * @charger: pointer to the ux500_charger structure + * @vset: charging voltage + * @iset: charger output current + * + * Check if the VBUS charger has been disconnected and reconnected without + * AB8500 rising an interrupt. Returns 0 on success. + */ +static int ab8500_charger_usb_check_enable(struct ux500_charger *charger, + int vset, int iset) +{ + u8 usbch_ctrl1 = 0; + int ret = 0; + + struct ab8500_charger *di = to_ab8500_charger_usb_device_info(charger); + + if (!di->usb.charger_connected) + return ret; + + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_USBCH_CTRL1_REG, &usbch_ctrl1); + if (ret < 0) { + dev_err(di->dev, "ab8500 read failed %d\n", __LINE__); + return ret; + } + dev_dbg(di->dev, "USB charger ctrl: 0x%02x\n", usbch_ctrl1); + + if (!(usbch_ctrl1 & USB_CH_ENA)) { + dev_info(di->dev, "Charging has been disabled abnormally and will be re-enabled\n"); + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CHARGER_CTRL, + DROP_COUNT_RESET, DROP_COUNT_RESET); + if (ret < 0) { + dev_err(di->dev, "ab8500 write failed %d\n", __LINE__); + return ret; + } + + ret = ab8500_charger_usb_en(&di->usb_chg, true, vset, iset); + if (ret < 0) { + dev_err(di->dev, "Failed to enable VBUS charger %d\n", + __LINE__); + return ret; + } + } + return ret; +} + +/** + * ab8500_charger_ac_check_enable() - enable usb charging + * @charger: pointer to the ux500_charger structure + * @vset: charging voltage + * @iset: charger output current + * + * Check if the AC charger has been disconnected and reconnected without + * AB8500 rising an interrupt. Returns 0 on success. + */ +static int ab8500_charger_ac_check_enable(struct ux500_charger *charger, + int vset, int iset) +{ + u8 mainch_ctrl1 = 0; + int ret = 0; + + struct ab8500_charger *di = to_ab8500_charger_ac_device_info(charger); + + if (!di->ac.charger_connected) + return ret; + + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_MCH_CTRL1, &mainch_ctrl1); + if (ret < 0) { + dev_err(di->dev, "ab8500 read failed %d\n", __LINE__); + return ret; + } + dev_dbg(di->dev, "AC charger ctrl: 0x%02x\n", mainch_ctrl1); + + if (!(mainch_ctrl1 & MAIN_CH_ENA)) { + dev_info(di->dev, "Charging has been disabled abnormally and will be re-enabled\n"); + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CHARGER_CTRL, + DROP_COUNT_RESET, DROP_COUNT_RESET); + + if (ret < 0) { + dev_err(di->dev, "ab8500 write failed %d\n", __LINE__); + return ret; + } + + ret = ab8500_charger_ac_en(&di->usb_chg, true, vset, iset); + if (ret < 0) { + dev_err(di->dev, "failed to enable AC charger %d\n", + __LINE__); + return ret; + } + } + return ret; +} + +/** + * ab8500_charger_watchdog_kick() - kick charger watchdog + * @di: pointer to the ab8500_charger structure + * + * Kick charger watchdog + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_watchdog_kick(struct ux500_charger *charger) +{ + int ret; + struct ab8500_charger *di; + + if (charger->psy->desc->type == POWER_SUPPLY_TYPE_MAINS) + di = to_ab8500_charger_ac_device_info(charger); + else if (charger->psy->desc->type == POWER_SUPPLY_TYPE_USB) + di = to_ab8500_charger_usb_device_info(charger); + else + return -ENXIO; + + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CHARG_WD_CTRL, CHARG_WD_KICK); + if (ret) + dev_err(di->dev, "Failed to kick WD!\n"); + + return ret; +} + +/** + * ab8500_charger_update_charger_current() - update charger current + * @di: pointer to the ab8500_charger structure + * + * Update the charger output current for the specified charger + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_update_charger_current(struct ux500_charger *charger, + int ich_out) +{ + int ret; + struct ab8500_charger *di; + + if (charger->psy->desc->type == POWER_SUPPLY_TYPE_MAINS) + di = to_ab8500_charger_ac_device_info(charger); + else if (charger->psy->desc->type == POWER_SUPPLY_TYPE_USB) + di = to_ab8500_charger_usb_device_info(charger); + else + return -ENXIO; + + ret = ab8500_charger_set_output_curr(di, ich_out); + if (ret) { + dev_err(di->dev, "%s " + "Failed to set ChOutputCurentLevel\n", + __func__); + return ret; + } + + /* Reset the main and usb drop input current measurement counter */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CHARGER_CTRL, DROP_COUNT_RESET); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + return ret; + } + + return ret; +} + +static int ab8500_charger_get_ext_psy_data(struct device *dev, void *data) +{ + struct power_supply *psy; + struct power_supply *ext = dev_get_drvdata(dev); + const char **supplicants = (const char **)ext->supplied_to; + struct ab8500_charger *di; + union power_supply_propval ret; + int j; + struct ux500_charger *usb_chg; + + usb_chg = (struct ux500_charger *)data; + psy = usb_chg->psy; + + di = to_ab8500_charger_usb_device_info(usb_chg); + + /* For all psy where the driver name appears in any supplied_to */ + j = match_string(supplicants, ext->num_supplicants, psy->desc->name); + if (j < 0) + return 0; + + /* Go through all properties for the psy */ + for (j = 0; j < ext->desc->num_properties; j++) { + enum power_supply_property prop; + prop = ext->desc->properties[j]; + + if (power_supply_get_property(ext, prop, &ret)) + continue; + + switch (prop) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + di->vbat = ret.intval / 1000; + break; + default: + break; + } + break; + default: + break; + } + } + return 0; +} + +/** + * ab8500_charger_check_vbat_work() - keep vbus current within spec + * @work pointer to the work_struct structure + * + * Due to a asic bug it is necessary to lower the input current to the vbus + * charger when charging with at some specific levels. This issue is only valid + * for below a certain battery voltage. This function makes sure that the + * the allowed current limit isn't exceeded. + */ +static void ab8500_charger_check_vbat_work(struct work_struct *work) +{ + int t = 10; + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, check_vbat_work.work); + + class_for_each_device(power_supply_class, NULL, + di->usb_chg.psy, ab8500_charger_get_ext_psy_data); + + /* First run old_vbat is 0. */ + if (di->old_vbat == 0) + di->old_vbat = di->vbat; + + if (!((di->old_vbat <= VBAT_TRESH_IP_CUR_RED && + di->vbat <= VBAT_TRESH_IP_CUR_RED) || + (di->old_vbat > VBAT_TRESH_IP_CUR_RED && + di->vbat > VBAT_TRESH_IP_CUR_RED))) { + + dev_dbg(di->dev, "Vbat did cross threshold, curr: %d, new: %d," + " old: %d\n", di->max_usb_in_curr.usb_type_max, + di->vbat, di->old_vbat); + ab8500_charger_set_vbus_in_curr(di, + di->max_usb_in_curr.usb_type_max); + power_supply_changed(di->usb_chg.psy); + } + + di->old_vbat = di->vbat; + + /* + * No need to check the battery voltage every second when not close to + * the threshold. + */ + if (di->vbat < (VBAT_TRESH_IP_CUR_RED + 100) && + (di->vbat > (VBAT_TRESH_IP_CUR_RED - 100))) + t = 1; + + queue_delayed_work(di->charger_wq, &di->check_vbat_work, t * HZ); +} + +/** + * ab8500_charger_check_hw_failure_work() - check main charger failure + * @work: pointer to the work_struct structure + * + * Work queue function for checking the main charger status + */ +static void ab8500_charger_check_hw_failure_work(struct work_struct *work) +{ + int ret; + u8 reg_value; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, check_hw_failure_work.work); + + /* Check if the status bits for HW failure is still active */ + if (di->flags.mainextchnotok) { + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_STATUS2_REG, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + if (!(reg_value & MAIN_CH_NOK)) { + di->flags.mainextchnotok = false; + ab8500_power_supply_changed(di, di->ac_chg.psy); + } + } + if (di->flags.vbus_ovv) { + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_USBCH_STAT2_REG, + ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + if (!(reg_value & VBUS_OVV_TH)) { + di->flags.vbus_ovv = false; + ab8500_power_supply_changed(di, di->usb_chg.psy); + } + } + /* If we still have a failure, schedule a new check */ + if (di->flags.mainextchnotok || di->flags.vbus_ovv) { + queue_delayed_work(di->charger_wq, + &di->check_hw_failure_work, round_jiffies(HZ)); + } +} + +/** + * ab8500_charger_kick_watchdog_work() - kick the watchdog + * @work: pointer to the work_struct structure + * + * Work queue function for kicking the charger watchdog. + * + * For ABB revision 1.0 and 1.1 there is a bug in the watchdog + * logic. That means we have to continuously kick the charger + * watchdog even when no charger is connected. This is only + * valid once the AC charger has been enabled. This is + * a bug that is not handled by the algorithm and the + * watchdog have to be kicked by the charger driver + * when the AC charger is disabled + */ +static void ab8500_charger_kick_watchdog_work(struct work_struct *work) +{ + int ret; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, kick_wd_work.work); + + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CHARG_WD_CTRL, CHARG_WD_KICK); + if (ret) + dev_err(di->dev, "Failed to kick WD!\n"); + + /* Schedule a new watchdog kick */ + queue_delayed_work(di->charger_wq, + &di->kick_wd_work, round_jiffies(WD_KICK_INTERVAL)); +} + +/** + * ab8500_charger_ac_work() - work to get and set main charger status + * @work: pointer to the work_struct structure + * + * Work queue function for checking the main charger status + */ +static void ab8500_charger_ac_work(struct work_struct *work) +{ + int ret; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, ac_work); + + /* + * Since we can't be sure that the events are received + * synchronously, we have the check if the main charger is + * connected by reading the status register + */ + ret = ab8500_charger_detect_chargers(di, false); + if (ret < 0) + return; + + if (ret & AC_PW_CONN) { + di->ac.charger_connected = 1; + di->ac_conn = true; + } else { + di->ac.charger_connected = 0; + } + + ab8500_power_supply_changed(di, di->ac_chg.psy); + sysfs_notify(&di->ac_chg.psy->dev.kobj, NULL, "present"); +} + +static void ab8500_charger_usb_attached_work(struct work_struct *work) +{ + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, + usb_charger_attached_work.work); + int usbch = (USB_CH_VBUSDROP | USB_CH_VBUSDETDBNC); + int ret, i; + u8 statval; + + for (i = 0; i < 10; i++) { + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_CH_USBCH_STAT1_REG, + &statval); + if (ret < 0) { + dev_err(di->dev, "ab8500 read failed %d\n", __LINE__); + goto reschedule; + } + if ((statval & usbch) != usbch) + goto reschedule; + + msleep(CHARGER_STATUS_POLL); + } + + ab8500_charger_usb_en(&di->usb_chg, 0, 0, 0); + + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + + return; + +reschedule: + queue_delayed_work(di->charger_wq, + &di->usb_charger_attached_work, + HZ); +} + +static void ab8500_charger_ac_attached_work(struct work_struct *work) +{ + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, + ac_charger_attached_work.work); + int mainch = (MAIN_CH_STATUS2_MAINCHGDROP | + MAIN_CH_STATUS2_MAINCHARGERDETDBNC); + int ret, i; + u8 statval; + + for (i = 0; i < 10; i++) { + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_CH_STATUS2_REG, + &statval); + if (ret < 0) { + dev_err(di->dev, "ab8500 read failed %d\n", __LINE__); + goto reschedule; + } + + if ((statval & mainch) != mainch) + goto reschedule; + + msleep(CHARGER_STATUS_POLL); + } + + ab8500_charger_ac_en(&di->ac_chg, 0, 0, 0); + queue_work(di->charger_wq, &di->ac_work); + + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + + return; + +reschedule: + queue_delayed_work(di->charger_wq, + &di->ac_charger_attached_work, + HZ); +} + +/** + * ab8500_charger_detect_usb_type_work() - work to detect USB type + * @work: Pointer to the work_struct structure + * + * Detect the type of USB plugged + */ +static void ab8500_charger_detect_usb_type_work(struct work_struct *work) +{ + int ret; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, detect_usb_type_work); + + /* + * Since we can't be sure that the events are received + * synchronously, we have the check if is + * connected by reading the status register + */ + ret = ab8500_charger_detect_chargers(di, false); + if (ret < 0) + return; + + if (!(ret & USB_PW_CONN)) { + dev_dbg(di->dev, "%s di->vbus_detected = false\n", __func__); + di->vbus_detected = false; + ab8500_charger_set_usb_connected(di, false); + ab8500_power_supply_changed(di, di->usb_chg.psy); + } else { + dev_dbg(di->dev, "%s di->vbus_detected = true\n", __func__); + di->vbus_detected = true; + + if (is_ab8500_1p1_or_earlier(di->parent)) { + ret = ab8500_charger_detect_usb_type(di); + if (!ret) { + ab8500_charger_set_usb_connected(di, true); + ab8500_power_supply_changed(di, + di->usb_chg.psy); + } + } else { + /* + * For ABB cut2.0 and onwards we have an IRQ, + * USB_LINK_STATUS that will be triggered when the USB + * link status changes. The exception is USB connected + * during startup. Then we don't get a + * USB_LINK_STATUS IRQ + */ + if (di->vbus_detected_start) { + di->vbus_detected_start = false; + ret = ab8500_charger_detect_usb_type(di); + if (!ret) { + ab8500_charger_set_usb_connected(di, + true); + ab8500_power_supply_changed(di, + di->usb_chg.psy); + } + } + } + } +} + +/** + * ab8500_charger_usb_link_attach_work() - work to detect USB type + * @work: pointer to the work_struct structure + * + * Detect the type of USB plugged + */ +static void ab8500_charger_usb_link_attach_work(struct work_struct *work) +{ + struct ab8500_charger *di = + container_of(work, struct ab8500_charger, attach_work.work); + int ret; + + /* Update maximum input current if USB enumeration is not detected */ + if (!di->usb.charger_online) { + ret = ab8500_charger_set_vbus_in_curr(di, + di->max_usb_in_curr.usb_type_max); + if (ret) + return; + } + + ab8500_charger_set_usb_connected(di, true); + ab8500_power_supply_changed(di, di->usb_chg.psy); +} + +/** + * ab8500_charger_usb_link_status_work() - work to detect USB type + * @work: pointer to the work_struct structure + * + * Detect the type of USB plugged + */ +static void ab8500_charger_usb_link_status_work(struct work_struct *work) +{ + int detected_chargers; + int ret; + u8 val; + u8 link_status; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, usb_link_status_work); + + /* + * Since we can't be sure that the events are received + * synchronously, we have the check if is + * connected by reading the status register + */ + detected_chargers = ab8500_charger_detect_chargers(di, false); + if (detected_chargers < 0) + return; + + /* + * Some chargers that breaks the USB spec is + * identified as invalid by AB8500 and it refuse + * to start the charging process. but by jumping + * through a few hoops it can be forced to start. + */ + if (is_ab8500(di->parent)) + ret = abx500_get_register_interruptible(di->dev, AB8500_USB, + AB8500_USB_LINE_STAT_REG, &val); + else + ret = abx500_get_register_interruptible(di->dev, AB8500_USB, + AB8500_USB_LINK1_STAT_REG, &val); + + if (ret >= 0) + dev_dbg(di->dev, "UsbLineStatus register = 0x%02x\n", val); + else + dev_dbg(di->dev, "Error reading USB link status\n"); + + if (is_ab8500(di->parent)) + link_status = AB8500_USB_LINK_STATUS; + else + link_status = AB8505_USB_LINK_STATUS; + + if (detected_chargers & USB_PW_CONN) { + if (((val & link_status) >> USB_LINK_STATUS_SHIFT) == + USB_STAT_NOT_VALID_LINK && + di->invalid_charger_detect_state == 0) { + dev_dbg(di->dev, + "Invalid charger detected, state= 0\n"); + /*Enable charger*/ + abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_USBCH_CTRL1_REG, + USB_CH_ENA, USB_CH_ENA); + /*Enable charger detection*/ + abx500_mask_and_set_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINE_CTRL2_REG, + USB_CH_DET, USB_CH_DET); + di->invalid_charger_detect_state = 1; + /*exit and wait for new link status interrupt.*/ + return; + + } + if (di->invalid_charger_detect_state == 1) { + dev_dbg(di->dev, + "Invalid charger detected, state= 1\n"); + /*Stop charger detection*/ + abx500_mask_and_set_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINE_CTRL2_REG, + USB_CH_DET, 0x00); + /*Check link status*/ + if (is_ab8500(di->parent)) + ret = abx500_get_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINE_STAT_REG, + &val); + else + ret = abx500_get_register_interruptible(di->dev, + AB8500_USB, AB8500_USB_LINK1_STAT_REG, + &val); + + dev_dbg(di->dev, "USB link status= 0x%02x\n", + (val & link_status) >> USB_LINK_STATUS_SHIFT); + di->invalid_charger_detect_state = 2; + } + } else { + di->invalid_charger_detect_state = 0; + } + + if (!(detected_chargers & USB_PW_CONN)) { + di->vbus_detected = false; + ab8500_charger_set_usb_connected(di, false); + ab8500_power_supply_changed(di, di->usb_chg.psy); + return; + } + + dev_dbg(di->dev,"%s di->vbus_detected = true\n",__func__); + di->vbus_detected = true; + ret = ab8500_charger_read_usb_type(di); + if (ret) { + if (ret == -ENXIO) { + /* No valid charger type detected */ + ab8500_charger_set_usb_connected(di, false); + ab8500_power_supply_changed(di, di->usb_chg.psy); + } + return; + } + + if (di->usb_device_is_unrecognised) { + dev_dbg(di->dev, + "Potential Legacy Charger device. " + "Delay work for %d msec for USB enum " + "to finish", + WAIT_ACA_RID_ENUMERATION); + queue_delayed_work(di->charger_wq, + &di->attach_work, + msecs_to_jiffies(WAIT_ACA_RID_ENUMERATION)); + } else if (di->is_aca_rid == 1) { + /* Only wait once */ + di->is_aca_rid++; + dev_dbg(di->dev, + "%s Wait %d msec for USB enum to finish", + __func__, WAIT_ACA_RID_ENUMERATION); + queue_delayed_work(di->charger_wq, + &di->attach_work, + msecs_to_jiffies(WAIT_ACA_RID_ENUMERATION)); + } else { + queue_delayed_work(di->charger_wq, + &di->attach_work, + 0); + } +} + +static void ab8500_charger_usb_state_changed_work(struct work_struct *work) +{ + int ret; + unsigned long flags; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, usb_state_changed_work.work); + + if (!di->vbus_detected) { + dev_dbg(di->dev, + "%s !di->vbus_detected\n", + __func__); + return; + } + + spin_lock_irqsave(&di->usb_state.usb_lock, flags); + di->usb_state.state = di->usb_state.state_tmp; + di->usb_state.usb_current = di->usb_state.usb_current_tmp; + spin_unlock_irqrestore(&di->usb_state.usb_lock, flags); + + dev_dbg(di->dev, "%s USB state: 0x%02x mA: %d\n", + __func__, di->usb_state.state, di->usb_state.usb_current); + + switch (di->usb_state.state) { + case AB8500_BM_USB_STATE_RESET_HS: + case AB8500_BM_USB_STATE_RESET_FS: + case AB8500_BM_USB_STATE_SUSPEND: + case AB8500_BM_USB_STATE_MAX: + ab8500_charger_set_usb_connected(di, false); + ab8500_power_supply_changed(di, di->usb_chg.psy); + break; + + case AB8500_BM_USB_STATE_RESUME: + /* + * when suspend->resume there should be delay + * of 1sec for enabling charging + */ + msleep(1000); + fallthrough; + case AB8500_BM_USB_STATE_CONFIGURED: + /* + * USB is configured, enable charging with the charging + * input current obtained from USB driver + */ + if (!ab8500_charger_get_usb_cur(di)) { + /* Update maximum input current */ + ret = ab8500_charger_set_vbus_in_curr(di, + di->max_usb_in_curr.usb_type_max); + if (ret) + return; + + ab8500_charger_set_usb_connected(di, true); + ab8500_power_supply_changed(di, di->usb_chg.psy); + } + break; + + default: + break; + } +} + +/** + * ab8500_charger_check_usbchargernotok_work() - check USB chg not ok status + * @work: pointer to the work_struct structure + * + * Work queue function for checking the USB charger Not OK status + */ +static void ab8500_charger_check_usbchargernotok_work(struct work_struct *work) +{ + int ret; + u8 reg_value; + bool prev_status; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, check_usbchgnotok_work.work); + + /* Check if the status bit for usbchargernotok is still active */ + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_USBCH_STAT2_REG, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + prev_status = di->flags.usbchargernotok; + + if (reg_value & VBUS_CH_NOK) { + di->flags.usbchargernotok = true; + /* Check again in 1sec */ + queue_delayed_work(di->charger_wq, + &di->check_usbchgnotok_work, HZ); + } else { + di->flags.usbchargernotok = false; + di->flags.vbus_collapse = false; + } + + if (prev_status != di->flags.usbchargernotok) + ab8500_power_supply_changed(di, di->usb_chg.psy); +} + +/** + * ab8500_charger_check_main_thermal_prot_work() - check main thermal status + * @work: pointer to the work_struct structure + * + * Work queue function for checking the Main thermal prot status + */ +static void ab8500_charger_check_main_thermal_prot_work( + struct work_struct *work) +{ + int ret; + u8 reg_value; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, check_main_thermal_prot_work); + + /* Check if the status bit for main_thermal_prot is still active */ + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_STATUS2_REG, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + if (reg_value & MAIN_CH_TH_PROT) + di->flags.main_thermal_prot = true; + else + di->flags.main_thermal_prot = false; + + ab8500_power_supply_changed(di, di->ac_chg.psy); +} + +/** + * ab8500_charger_check_usb_thermal_prot_work() - check usb thermal status + * @work: pointer to the work_struct structure + * + * Work queue function for checking the USB thermal prot status + */ +static void ab8500_charger_check_usb_thermal_prot_work( + struct work_struct *work) +{ + int ret; + u8 reg_value; + + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, check_usb_thermal_prot_work); + + /* Check if the status bit for usb_thermal_prot is still active */ + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_USBCH_STAT2_REG, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + if (reg_value & USB_CH_TH_PROT) + di->flags.usb_thermal_prot = true; + else + di->flags.usb_thermal_prot = false; + + ab8500_power_supply_changed(di, di->usb_chg.psy); +} + +/** + * ab8500_charger_mainchunplugdet_handler() - main charger unplugged + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_mainchunplugdet_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "Main charger unplugged\n"); + queue_work(di->charger_wq, &di->ac_work); + + cancel_delayed_work_sync(&di->ac_charger_attached_work); + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_mainchplugdet_handler() - main charger plugged + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_mainchplugdet_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "Main charger plugged\n"); + queue_work(di->charger_wq, &di->ac_work); + + mutex_lock(&di->charger_attached_mutex); + mutex_unlock(&di->charger_attached_mutex); + + if (is_ab8500(di->parent)) + queue_delayed_work(di->charger_wq, + &di->ac_charger_attached_work, + HZ); + return IRQ_HANDLED; +} + +/** + * ab8500_charger_mainextchnotok_handler() - main charger not ok + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_mainextchnotok_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "Main charger not ok\n"); + di->flags.mainextchnotok = true; + ab8500_power_supply_changed(di, di->ac_chg.psy); + + /* Schedule a new HW failure check */ + queue_delayed_work(di->charger_wq, &di->check_hw_failure_work, 0); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_mainchthprotr_handler() - Die temp is above main charger + * thermal protection threshold + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_mainchthprotr_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, + "Die temp above Main charger thermal protection threshold\n"); + queue_work(di->charger_wq, &di->check_main_thermal_prot_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_mainchthprotf_handler() - Die temp is below main charger + * thermal protection threshold + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_mainchthprotf_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, + "Die temp ok for Main charger thermal protection threshold\n"); + queue_work(di->charger_wq, &di->check_main_thermal_prot_work); + + return IRQ_HANDLED; +} + +static void ab8500_charger_vbus_drop_end_work(struct work_struct *work) +{ + struct ab8500_charger *di = container_of(work, + struct ab8500_charger, vbus_drop_end_work.work); + int ret, curr; + u8 reg_value; + + di->flags.vbus_drop_end = false; + + /* Reset the drop counter */ + abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CHARGER_CTRL, 0x01); + + ret = abx500_get_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_USBCH_STAT2_REG, ®_value); + if (ret < 0) { + dev_err(di->dev, "%s read failed\n", __func__); + return; + } + + curr = di->bm->chg_input_curr[ + reg_value >> AUTO_VBUS_IN_CURR_LIM_SHIFT]; + + if (di->max_usb_in_curr.calculated_max != curr) { + /* USB source is collapsing */ + di->max_usb_in_curr.calculated_max = curr; + dev_dbg(di->dev, + "VBUS input current limiting to %d mA\n", + di->max_usb_in_curr.calculated_max); + } else { + /* + * USB source can not give more than this amount. + * Taking more will collapse the source. + */ + di->max_usb_in_curr.set_max = + di->max_usb_in_curr.calculated_max; + dev_dbg(di->dev, + "VBUS input current limited to %d mA\n", + di->max_usb_in_curr.set_max); + } + + if (di->usb.charger_connected) + ab8500_charger_set_vbus_in_curr(di, + di->max_usb_in_curr.usb_type_max); +} + +/** + * ab8500_charger_vbusdetf_handler() - VBUS falling detected + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_vbusdetf_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + di->vbus_detected = false; + dev_dbg(di->dev, "VBUS falling detected\n"); + queue_work(di->charger_wq, &di->detect_usb_type_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_vbusdetr_handler() - VBUS rising detected + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_vbusdetr_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + di->vbus_detected = true; + dev_dbg(di->dev, "VBUS rising detected\n"); + + queue_work(di->charger_wq, &di->detect_usb_type_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_usblinkstatus_handler() - USB link status has changed + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_usblinkstatus_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "USB link status changed\n"); + + queue_work(di->charger_wq, &di->usb_link_status_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_usbchthprotr_handler() - Die temp is above usb charger + * thermal protection threshold + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_usbchthprotr_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, + "Die temp above USB charger thermal protection threshold\n"); + queue_work(di->charger_wq, &di->check_usb_thermal_prot_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_usbchthprotf_handler() - Die temp is below usb charger + * thermal protection threshold + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_usbchthprotf_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, + "Die temp ok for USB charger thermal protection threshold\n"); + queue_work(di->charger_wq, &di->check_usb_thermal_prot_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_usbchargernotokr_handler() - USB charger not ok detected + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_usbchargernotokr_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "Not allowed USB charger detected\n"); + queue_delayed_work(di->charger_wq, &di->check_usbchgnotok_work, 0); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_chwdexp_handler() - Charger watchdog expired + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_chwdexp_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "Charger watchdog expired\n"); + + /* + * The charger that was online when the watchdog expired + * needs to be restarted for charging to start again + */ + if (di->ac.charger_online) { + di->ac.wd_expired = true; + ab8500_power_supply_changed(di, di->ac_chg.psy); + } + if (di->usb.charger_online) { + di->usb.wd_expired = true; + ab8500_power_supply_changed(di, di->usb_chg.psy); + } + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_vbuschdropend_handler() - VBUS drop removed + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_vbuschdropend_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "VBUS charger drop ended\n"); + di->flags.vbus_drop_end = true; + + /* + * VBUS might have dropped due to bad connection. + * Schedule a new input limit set to the value SW requests. + */ + queue_delayed_work(di->charger_wq, &di->vbus_drop_end_work, + round_jiffies(VBUS_IN_CURR_LIM_RETRY_SET_TIME * HZ)); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_vbusovv_handler() - VBUS overvoltage detected + * @irq: interrupt number + * @_di: pointer to the ab8500_charger structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_charger_vbusovv_handler(int irq, void *_di) +{ + struct ab8500_charger *di = _di; + + dev_dbg(di->dev, "VBUS overvoltage detected\n"); + di->flags.vbus_ovv = true; + ab8500_power_supply_changed(di, di->usb_chg.psy); + + /* Schedule a new HW failure check */ + queue_delayed_work(di->charger_wq, &di->check_hw_failure_work, 0); + + return IRQ_HANDLED; +} + +/** + * ab8500_charger_ac_get_property() - get the ac/mains properties + * @psy: pointer to the power_supply structure + * @psp: pointer to the power_supply_property structure + * @val: pointer to the power_supply_propval union + * + * This function gets called when an application tries to get the ac/mains + * properties by reading the sysfs files. + * AC/Mains properties are online, present and voltage. + * online: ac/mains charging is in progress or not + * present: presence of the ac/mains + * voltage: AC/Mains voltage + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ab8500_charger *di; + int ret; + + di = to_ab8500_charger_ac_device_info(psy_to_ux500_charger(psy)); + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + if (di->flags.mainextchnotok) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else if (di->ac.wd_expired || di->usb.wd_expired) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (di->flags.main_thermal_prot) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = di->ac.charger_online; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = di->ac.charger_connected; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = ab8500_charger_get_ac_voltage(di); + if (ret >= 0) + di->ac.charger_voltage = ret; + /* On error, use previous value */ + val->intval = di->ac.charger_voltage * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + /* + * This property is used to indicate when CV mode is entered + * for the AC charger + */ + di->ac.cv_active = ab8500_charger_ac_cv(di); + val->intval = di->ac.cv_active; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = ab8500_charger_get_ac_current(di); + if (ret >= 0) + di->ac.charger_current = ret; + val->intval = di->ac.charger_current * 1000; + break; + default: + return -EINVAL; + } + return 0; +} + +/** + * ab8500_charger_usb_get_property() - get the usb properties + * @psy: pointer to the power_supply structure + * @psp: pointer to the power_supply_property structure + * @val: pointer to the power_supply_propval union + * + * This function gets called when an application tries to get the usb + * properties by reading the sysfs files. + * USB properties are online, present and voltage. + * online: usb charging is in progress or not + * present: presence of the usb + * voltage: vbus voltage + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_charger_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ab8500_charger *di; + int ret; + + di = to_ab8500_charger_usb_device_info(psy_to_ux500_charger(psy)); + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + if (di->flags.usbchargernotok) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else if (di->ac.wd_expired || di->usb.wd_expired) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (di->flags.usb_thermal_prot) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (di->flags.vbus_ovv) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = di->usb.charger_online; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = di->usb.charger_connected; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = ab8500_charger_get_vbus_voltage(di); + if (ret >= 0) + di->usb.charger_voltage = ret; + val->intval = di->usb.charger_voltage * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + /* + * This property is used to indicate when CV mode is entered + * for the USB charger + */ + di->usb.cv_active = ab8500_charger_usb_cv(di); + val->intval = di->usb.cv_active; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = ab8500_charger_get_usb_current(di); + if (ret >= 0) + di->usb.charger_current = ret; + val->intval = di->usb.charger_current * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + /* + * This property is used to indicate when VBUS has collapsed + * due to too high output current from the USB charger + */ + if (di->flags.vbus_collapse) + val->intval = 1; + else + val->intval = 0; + break; + default: + return -EINVAL; + } + return 0; +} + +/** + * ab8500_charger_init_hw_registers() - Set up charger related registers + * @di: pointer to the ab8500_charger structure + * + * Set up charger OVV, watchdog and maximum voltage registers as well as + * charging of the backup battery + */ +static int ab8500_charger_init_hw_registers(struct ab8500_charger *di) +{ + int ret = 0; + + /* Setup maximum charger current and voltage for ABB cut2.0 */ + if (!is_ab8500_1p1_or_earlier(di->parent)) { + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_CH_VOLT_LVL_MAX_REG, CH_VOL_LVL_4P6); + if (ret) { + dev_err(di->dev, + "failed to set CH_VOLT_LVL_MAX_REG\n"); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_OPT_CRNTLVL_MAX_REG, + CH_OP_CUR_LVL_1P6); + if (ret) { + dev_err(di->dev, + "failed to set CH_OPT_CRNTLVL_MAX_REG\n"); + goto out; + } + } + + if (is_ab8505_2p0(di->parent)) + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_USBCH_CTRL2_REG, + VBUS_AUTO_IN_CURR_LIM_ENA, + VBUS_AUTO_IN_CURR_LIM_ENA); + else + /* + * VBUS OVV set to 6.3V and enable automatic current limitation + */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_USBCH_CTRL2_REG, + VBUS_OVV_SELECT_6P3V | VBUS_AUTO_IN_CURR_LIM_ENA); + if (ret) { + dev_err(di->dev, + "failed to set automatic current limitation\n"); + goto out; + } + + /* Enable main watchdog in OTP */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_OTP_EMUL, AB8500_OTP_CONF_15, OTP_ENABLE_WD); + if (ret) { + dev_err(di->dev, "failed to enable main WD in OTP\n"); + goto out; + } + + /* Enable main watchdog */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_SYS_CTRL2_BLOCK, + AB8500_MAIN_WDOG_CTRL_REG, MAIN_WDOG_ENA); + if (ret) { + dev_err(di->dev, "failed to enable main watchdog\n"); + goto out; + } + + /* + * Due to internal synchronisation, Enable and Kick watchdog bits + * cannot be enabled in a single write. + * A minimum delay of 2*32 kHz period (62.5µs) must be inserted + * between writing Enable then Kick bits. + */ + udelay(63); + + /* Kick main watchdog */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_SYS_CTRL2_BLOCK, + AB8500_MAIN_WDOG_CTRL_REG, + (MAIN_WDOG_ENA | MAIN_WDOG_KICK)); + if (ret) { + dev_err(di->dev, "failed to kick main watchdog\n"); + goto out; + } + + /* Disable main watchdog */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_SYS_CTRL2_BLOCK, + AB8500_MAIN_WDOG_CTRL_REG, MAIN_WDOG_DIS); + if (ret) { + dev_err(di->dev, "failed to disable main watchdog\n"); + goto out; + } + + /* Set watchdog timeout */ + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CH_WD_TIMER_REG, WD_TIMER); + if (ret) { + dev_err(di->dev, "failed to set charger watchdog timeout\n"); + goto out; + } + + ret = ab8500_charger_led_en(di, false); + if (ret < 0) { + dev_err(di->dev, "failed to disable LED\n"); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, + AB8500_RTC, + AB8500_RTC_BACKUP_CHG_REG, + (di->bm->bkup_bat_v & 0x3) | di->bm->bkup_bat_i); + if (ret) { + dev_err(di->dev, "failed to setup backup battery charging\n"); + goto out; + } + + /* Enable backup battery charging */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_RTC, AB8500_RTC_CTRL_REG, + RTC_BUP_CH_ENA, RTC_BUP_CH_ENA); + if (ret < 0) { + dev_err(di->dev, "%s mask and set failed\n", __func__); + goto out; + } + +out: + return ret; +} + +/* + * ab8500 charger driver interrupts and their respective isr + */ +static struct ab8500_charger_interrupts ab8500_charger_irq[] = { + {"MAIN_CH_UNPLUG_DET", ab8500_charger_mainchunplugdet_handler}, + {"MAIN_CHARGE_PLUG_DET", ab8500_charger_mainchplugdet_handler}, + {"MAIN_EXT_CH_NOT_OK", ab8500_charger_mainextchnotok_handler}, + {"MAIN_CH_TH_PROT_R", ab8500_charger_mainchthprotr_handler}, + {"MAIN_CH_TH_PROT_F", ab8500_charger_mainchthprotf_handler}, + {"VBUS_DET_F", ab8500_charger_vbusdetf_handler}, + {"VBUS_DET_R", ab8500_charger_vbusdetr_handler}, + {"USB_LINK_STATUS", ab8500_charger_usblinkstatus_handler}, + {"USB_CH_TH_PROT_R", ab8500_charger_usbchthprotr_handler}, + {"USB_CH_TH_PROT_F", ab8500_charger_usbchthprotf_handler}, + {"USB_CHARGER_NOT_OKR", ab8500_charger_usbchargernotokr_handler}, + {"VBUS_OVV", ab8500_charger_vbusovv_handler}, + {"CH_WD_EXP", ab8500_charger_chwdexp_handler}, + {"VBUS_CH_DROP_END", ab8500_charger_vbuschdropend_handler}, +}; + +static int ab8500_charger_usb_notifier_call(struct notifier_block *nb, + unsigned long event, void *power) +{ + struct ab8500_charger *di = + container_of(nb, struct ab8500_charger, nb); + enum ab8500_usb_state bm_usb_state; + unsigned mA = *((unsigned *)power); + + if (!di) + return NOTIFY_DONE; + + if (event != USB_EVENT_VBUS) { + dev_dbg(di->dev, "not a standard host, returning\n"); + return NOTIFY_DONE; + } + + /* TODO: State is fabricate here. See if charger really needs USB + * state or if mA is enough + */ + if ((di->usb_state.usb_current == 2) && (mA > 2)) + bm_usb_state = AB8500_BM_USB_STATE_RESUME; + else if (mA == 0) + bm_usb_state = AB8500_BM_USB_STATE_RESET_HS; + else if (mA == 2) + bm_usb_state = AB8500_BM_USB_STATE_SUSPEND; + else if (mA >= 8) /* 8, 100, 500 */ + bm_usb_state = AB8500_BM_USB_STATE_CONFIGURED; + else /* Should never occur */ + bm_usb_state = AB8500_BM_USB_STATE_RESET_FS; + + dev_dbg(di->dev, "%s usb_state: 0x%02x mA: %d\n", + __func__, bm_usb_state, mA); + + spin_lock(&di->usb_state.usb_lock); + di->usb_state.state_tmp = bm_usb_state; + di->usb_state.usb_current_tmp = mA; + spin_unlock(&di->usb_state.usb_lock); + + /* + * wait for some time until you get updates from the usb stack + * and negotiations are completed + */ + queue_delayed_work(di->charger_wq, &di->usb_state_changed_work, HZ/2); + + return NOTIFY_OK; +} + +#if defined(CONFIG_PM) +static int ab8500_charger_resume(struct platform_device *pdev) +{ + int ret; + struct ab8500_charger *di = platform_get_drvdata(pdev); + + /* + * For ABB revision 1.0 and 1.1 there is a bug in the watchdog + * logic. That means we have to continuously kick the charger + * watchdog even when no charger is connected. This is only + * valid once the AC charger has been enabled. This is + * a bug that is not handled by the algorithm and the + * watchdog have to be kicked by the charger driver + * when the AC charger is disabled + */ + if (di->ac_conn && is_ab8500_1p1_or_earlier(di->parent)) { + ret = abx500_set_register_interruptible(di->dev, AB8500_CHARGER, + AB8500_CHARG_WD_CTRL, CHARG_WD_KICK); + if (ret) + dev_err(di->dev, "Failed to kick WD!\n"); + + /* If not already pending start a new timer */ + queue_delayed_work(di->charger_wq, &di->kick_wd_work, + round_jiffies(WD_KICK_INTERVAL)); + } + + /* If we still have a HW failure, schedule a new check */ + if (di->flags.mainextchnotok || di->flags.vbus_ovv) { + queue_delayed_work(di->charger_wq, + &di->check_hw_failure_work, 0); + } + + if (di->flags.vbus_drop_end) + queue_delayed_work(di->charger_wq, &di->vbus_drop_end_work, 0); + + return 0; +} + +static int ab8500_charger_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct ab8500_charger *di = platform_get_drvdata(pdev); + + /* Cancel any pending jobs */ + cancel_delayed_work(&di->check_hw_failure_work); + cancel_delayed_work(&di->vbus_drop_end_work); + + flush_delayed_work(&di->attach_work); + flush_delayed_work(&di->usb_charger_attached_work); + flush_delayed_work(&di->ac_charger_attached_work); + flush_delayed_work(&di->check_usbchgnotok_work); + flush_delayed_work(&di->check_vbat_work); + flush_delayed_work(&di->kick_wd_work); + + flush_work(&di->usb_link_status_work); + flush_work(&di->ac_work); + flush_work(&di->detect_usb_type_work); + + if (atomic_read(&di->current_stepping_sessions)) + return -EAGAIN; + + return 0; +} +#else +#define ab8500_charger_suspend NULL +#define ab8500_charger_resume NULL +#endif + +static struct notifier_block charger_nb = { + .notifier_call = ab8500_external_charger_prepare, +}; + +static int ab8500_charger_remove(struct platform_device *pdev) +{ + struct ab8500_charger *di = platform_get_drvdata(pdev); + int i, irq, ret; + + /* Disable AC charging */ + ab8500_charger_ac_en(&di->ac_chg, false, 0, 0); + + /* Disable USB charging */ + ab8500_charger_usb_en(&di->usb_chg, false, 0, 0); + + /* Disable interrupts */ + for (i = 0; i < ARRAY_SIZE(ab8500_charger_irq); i++) { + irq = platform_get_irq_byname(pdev, ab8500_charger_irq[i].name); + free_irq(irq, di); + } + + /* Backup battery voltage and current disable */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_RTC, AB8500_RTC_CTRL_REG, RTC_BUP_CH_ENA, 0); + if (ret < 0) + dev_err(di->dev, "%s mask and set failed\n", __func__); + + usb_unregister_notifier(di->usb_phy, &di->nb); + usb_put_phy(di->usb_phy); + + /* Delete the work queue */ + destroy_workqueue(di->charger_wq); + + /* Unregister external charger enable notifier */ + if (!di->ac_chg.enabled) + blocking_notifier_chain_unregister( + &charger_notifier_list, &charger_nb); + + flush_scheduled_work(); + if (di->usb_chg.enabled) + power_supply_unregister(di->usb_chg.psy); + + if (di->ac_chg.enabled && !di->ac_chg.external) + power_supply_unregister(di->ac_chg.psy); + + return 0; +} + +static char *supply_interface[] = { + "ab8500_chargalg", + "ab8500_fg", + "ab8500_btemp", +}; + +static const struct power_supply_desc ab8500_ac_chg_desc = { + .name = "ab8500_ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = ab8500_charger_ac_props, + .num_properties = ARRAY_SIZE(ab8500_charger_ac_props), + .get_property = ab8500_charger_ac_get_property, +}; + +static const struct power_supply_desc ab8500_usb_chg_desc = { + .name = "ab8500_usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = ab8500_charger_usb_props, + .num_properties = ARRAY_SIZE(ab8500_charger_usb_props), + .get_property = ab8500_charger_usb_get_property, +}; + +static int ab8500_charger_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct abx500_bm_data *plat = pdev->dev.platform_data; + struct power_supply_config ac_psy_cfg = {}, usb_psy_cfg = {}; + struct ab8500_charger *di; + int irq, i, charger_status, ret = 0, ch_stat; + + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); + if (!di) { + dev_err(&pdev->dev, "%s no mem for ab8500_charger\n", __func__); + return -ENOMEM; + } + + if (!plat) { + dev_err(&pdev->dev, "no battery management data supplied\n"); + return -EINVAL; + } + di->bm = plat; + + if (np) { + ret = ab8500_bm_of_probe(&pdev->dev, np, di->bm); + if (ret) { + dev_err(&pdev->dev, "failed to get battery information\n"); + return ret; + } + di->autopower_cfg = of_property_read_bool(np, "autopower_cfg"); + } else + di->autopower_cfg = false; + + /* get parent data */ + di->dev = &pdev->dev; + di->parent = dev_get_drvdata(pdev->dev.parent); + + /* Get ADC channels */ + di->adc_main_charger_v = devm_iio_channel_get(&pdev->dev, + "main_charger_v"); + if (IS_ERR(di->adc_main_charger_v)) { + if (PTR_ERR(di->adc_main_charger_v) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get ADC main charger voltage\n"); + return PTR_ERR(di->adc_main_charger_v); + } + di->adc_main_charger_c = devm_iio_channel_get(&pdev->dev, + "main_charger_c"); + if (IS_ERR(di->adc_main_charger_c)) { + if (PTR_ERR(di->adc_main_charger_c) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get ADC main charger current\n"); + return PTR_ERR(di->adc_main_charger_c); + } + di->adc_vbus_v = devm_iio_channel_get(&pdev->dev, "vbus_v"); + if (IS_ERR(di->adc_vbus_v)) { + if (PTR_ERR(di->adc_vbus_v) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get ADC USB charger voltage\n"); + return PTR_ERR(di->adc_vbus_v); + } + di->adc_usb_charger_c = devm_iio_channel_get(&pdev->dev, + "usb_charger_c"); + if (IS_ERR(di->adc_usb_charger_c)) { + if (PTR_ERR(di->adc_usb_charger_c) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get ADC USB charger current\n"); + return PTR_ERR(di->adc_usb_charger_c); + } + + /* initialize lock */ + spin_lock_init(&di->usb_state.usb_lock); + mutex_init(&di->usb_ipt_crnt_lock); + + di->autopower = false; + di->invalid_charger_detect_state = 0; + + /* AC and USB supply config */ + ac_psy_cfg.supplied_to = supply_interface; + ac_psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); + ac_psy_cfg.drv_data = &di->ac_chg; + usb_psy_cfg.supplied_to = supply_interface; + usb_psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); + usb_psy_cfg.drv_data = &di->usb_chg; + + /* AC supply */ + /* ux500_charger sub-class */ + di->ac_chg.ops.enable = &ab8500_charger_ac_en; + di->ac_chg.ops.check_enable = &ab8500_charger_ac_check_enable; + di->ac_chg.ops.kick_wd = &ab8500_charger_watchdog_kick; + di->ac_chg.ops.update_curr = &ab8500_charger_update_charger_current; + di->ac_chg.max_out_volt = ab8500_charger_voltage_map[ + ARRAY_SIZE(ab8500_charger_voltage_map) - 1]; + di->ac_chg.max_out_curr = + di->bm->chg_output_curr[di->bm->n_chg_out_curr - 1]; + di->ac_chg.wdt_refresh = CHG_WD_INTERVAL; + di->ac_chg.enabled = di->bm->ac_enabled; + di->ac_chg.external = false; + + /*notifier for external charger enabling*/ + if (!di->ac_chg.enabled) + blocking_notifier_chain_register( + &charger_notifier_list, &charger_nb); + + /* USB supply */ + /* ux500_charger sub-class */ + di->usb_chg.ops.enable = &ab8500_charger_usb_en; + di->usb_chg.ops.check_enable = &ab8500_charger_usb_check_enable; + di->usb_chg.ops.kick_wd = &ab8500_charger_watchdog_kick; + di->usb_chg.ops.update_curr = &ab8500_charger_update_charger_current; + di->usb_chg.max_out_volt = ab8500_charger_voltage_map[ + ARRAY_SIZE(ab8500_charger_voltage_map) - 1]; + di->usb_chg.max_out_curr = + di->bm->chg_output_curr[di->bm->n_chg_out_curr - 1]; + di->usb_chg.wdt_refresh = CHG_WD_INTERVAL; + di->usb_chg.enabled = di->bm->usb_enabled; + di->usb_chg.external = false; + di->usb_state.usb_current = -1; + + /* Create a work queue for the charger */ + di->charger_wq = alloc_ordered_workqueue("ab8500_charger_wq", + WQ_MEM_RECLAIM); + if (di->charger_wq == NULL) { + dev_err(di->dev, "failed to create work queue\n"); + return -ENOMEM; + } + + mutex_init(&di->charger_attached_mutex); + + /* Init work for HW failure check */ + INIT_DEFERRABLE_WORK(&di->check_hw_failure_work, + ab8500_charger_check_hw_failure_work); + INIT_DEFERRABLE_WORK(&di->check_usbchgnotok_work, + ab8500_charger_check_usbchargernotok_work); + + INIT_DELAYED_WORK(&di->ac_charger_attached_work, + ab8500_charger_ac_attached_work); + INIT_DELAYED_WORK(&di->usb_charger_attached_work, + ab8500_charger_usb_attached_work); + + /* + * For ABB revision 1.0 and 1.1 there is a bug in the watchdog + * logic. That means we have to continuously kick the charger + * watchdog even when no charger is connected. This is only + * valid once the AC charger has been enabled. This is + * a bug that is not handled by the algorithm and the + * watchdog have to be kicked by the charger driver + * when the AC charger is disabled + */ + INIT_DEFERRABLE_WORK(&di->kick_wd_work, + ab8500_charger_kick_watchdog_work); + + INIT_DEFERRABLE_WORK(&di->check_vbat_work, + ab8500_charger_check_vbat_work); + + INIT_DELAYED_WORK(&di->attach_work, + ab8500_charger_usb_link_attach_work); + + INIT_DELAYED_WORK(&di->usb_state_changed_work, + ab8500_charger_usb_state_changed_work); + + INIT_DELAYED_WORK(&di->vbus_drop_end_work, + ab8500_charger_vbus_drop_end_work); + + /* Init work for charger detection */ + INIT_WORK(&di->usb_link_status_work, + ab8500_charger_usb_link_status_work); + INIT_WORK(&di->ac_work, ab8500_charger_ac_work); + INIT_WORK(&di->detect_usb_type_work, + ab8500_charger_detect_usb_type_work); + + /* Init work for checking HW status */ + INIT_WORK(&di->check_main_thermal_prot_work, + ab8500_charger_check_main_thermal_prot_work); + INIT_WORK(&di->check_usb_thermal_prot_work, + ab8500_charger_check_usb_thermal_prot_work); + + /* + * VDD ADC supply needs to be enabled from this driver when there + * is a charger connected to avoid erroneous BTEMP_HIGH/LOW + * interrupts during charging + */ + di->regu = devm_regulator_get(di->dev, "vddadc"); + if (IS_ERR(di->regu)) { + ret = PTR_ERR(di->regu); + dev_err(di->dev, "failed to get vddadc regulator\n"); + goto free_charger_wq; + } + + + /* Initialize OVV, and other registers */ + ret = ab8500_charger_init_hw_registers(di); + if (ret) { + dev_err(di->dev, "failed to initialize ABB registers\n"); + goto free_charger_wq; + } + + /* Register AC charger class */ + if (di->ac_chg.enabled) { + di->ac_chg.psy = power_supply_register(di->dev, + &ab8500_ac_chg_desc, + &ac_psy_cfg); + if (IS_ERR(di->ac_chg.psy)) { + dev_err(di->dev, "failed to register AC charger\n"); + ret = PTR_ERR(di->ac_chg.psy); + goto free_charger_wq; + } + } + + /* Register USB charger class */ + if (di->usb_chg.enabled) { + di->usb_chg.psy = power_supply_register(di->dev, + &ab8500_usb_chg_desc, + &usb_psy_cfg); + if (IS_ERR(di->usb_chg.psy)) { + dev_err(di->dev, "failed to register USB charger\n"); + ret = PTR_ERR(di->usb_chg.psy); + goto free_ac; + } + } + + di->usb_phy = usb_get_phy(USB_PHY_TYPE_USB2); + if (IS_ERR_OR_NULL(di->usb_phy)) { + dev_err(di->dev, "failed to get usb transceiver\n"); + ret = -EINVAL; + goto free_usb; + } + di->nb.notifier_call = ab8500_charger_usb_notifier_call; + ret = usb_register_notifier(di->usb_phy, &di->nb); + if (ret) { + dev_err(di->dev, "failed to register usb notifier\n"); + goto put_usb_phy; + } + + /* Identify the connected charger types during startup */ + charger_status = ab8500_charger_detect_chargers(di, true); + if (charger_status & AC_PW_CONN) { + di->ac.charger_connected = 1; + di->ac_conn = true; + ab8500_power_supply_changed(di, di->ac_chg.psy); + sysfs_notify(&di->ac_chg.psy->dev.kobj, NULL, "present"); + } + + if (charger_status & USB_PW_CONN) { + di->vbus_detected = true; + di->vbus_detected_start = true; + queue_work(di->charger_wq, + &di->detect_usb_type_work); + } + + /* Register interrupts */ + for (i = 0; i < ARRAY_SIZE(ab8500_charger_irq); i++) { + irq = platform_get_irq_byname(pdev, ab8500_charger_irq[i].name); + if (irq < 0) { + ret = irq; + goto free_irq; + } + + ret = request_threaded_irq(irq, NULL, ab8500_charger_irq[i].isr, + IRQF_SHARED | IRQF_NO_SUSPEND, + ab8500_charger_irq[i].name, di); + + if (ret != 0) { + dev_err(di->dev, "failed to request %s IRQ %d: %d\n" + , ab8500_charger_irq[i].name, irq, ret); + goto free_irq; + } + dev_dbg(di->dev, "Requested %s IRQ %d: %d\n", + ab8500_charger_irq[i].name, irq, ret); + } + + platform_set_drvdata(pdev, di); + + mutex_lock(&di->charger_attached_mutex); + + ch_stat = ab8500_charger_detect_chargers(di, false); + + if ((ch_stat & AC_PW_CONN) == AC_PW_CONN) { + if (is_ab8500(di->parent)) + queue_delayed_work(di->charger_wq, + &di->ac_charger_attached_work, + HZ); + } + if ((ch_stat & USB_PW_CONN) == USB_PW_CONN) { + if (is_ab8500(di->parent)) + queue_delayed_work(di->charger_wq, + &di->usb_charger_attached_work, + HZ); + } + + mutex_unlock(&di->charger_attached_mutex); + + return ret; + +free_irq: + usb_unregister_notifier(di->usb_phy, &di->nb); + + /* We also have to free all successfully registered irqs */ + for (i = i - 1; i >= 0; i--) { + irq = platform_get_irq_byname(pdev, ab8500_charger_irq[i].name); + free_irq(irq, di); + } +put_usb_phy: + usb_put_phy(di->usb_phy); +free_usb: + if (di->usb_chg.enabled) + power_supply_unregister(di->usb_chg.psy); +free_ac: + if (di->ac_chg.enabled) + power_supply_unregister(di->ac_chg.psy); +free_charger_wq: + destroy_workqueue(di->charger_wq); + return ret; +} + +static const struct of_device_id ab8500_charger_match[] = { + { .compatible = "stericsson,ab8500-charger", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ab8500_charger_match); + +static struct platform_driver ab8500_charger_driver = { + .probe = ab8500_charger_probe, + .remove = ab8500_charger_remove, + .suspend = ab8500_charger_suspend, + .resume = ab8500_charger_resume, + .driver = { + .name = "ab8500-charger", + .of_match_table = ab8500_charger_match, + }, +}; + +static int __init ab8500_charger_init(void) +{ + return platform_driver_register(&ab8500_charger_driver); +} + +static void __exit ab8500_charger_exit(void) +{ + platform_driver_unregister(&ab8500_charger_driver); +} + +subsys_initcall_sync(ab8500_charger_init); +module_exit(ab8500_charger_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Johan Palsson, Karl Komierowski, Arun R Murthy"); +MODULE_ALIAS("platform:ab8500-charger"); +MODULE_DESCRIPTION("AB8500 charger management driver"); diff --git a/drivers/power/supply/ab8500_fg.c b/drivers/power/supply/ab8500_fg.c new file mode 100644 index 000000000..a88590563 --- /dev/null +++ b/drivers/power/supply/ab8500_fg.c @@ -0,0 +1,3281 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) ST-Ericsson AB 2012 + * + * Main and Back-up battery management driver. + * + * Note: Backup battery management is required in case of Li-Ion battery and not + * for capacitive battery. HREF boards have capacitive battery and hence backup + * battery management is not used and the supported code is available in this + * driver. + * + * Author: + * Johan Palsson <johan.palsson@stericsson.com> + * Karl Komierowski <karl.komierowski@stericsson.com> + * Arun R Murthy <arun.murthy@stericsson.com> + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/kobject.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/time.h> +#include <linux/time64.h> +#include <linux/of.h> +#include <linux/completion.h> +#include <linux/mfd/core.h> +#include <linux/mfd/abx500.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500/ab8500-bm.h> +#include <linux/iio/consumer.h> +#include <linux/kernel.h> + +#define MILLI_TO_MICRO 1000 +#define FG_LSB_IN_MA 1627 +#define QLSB_NANO_AMP_HOURS_X10 1071 +#define INS_CURR_TIMEOUT (3 * HZ) + +#define SEC_TO_SAMPLE(S) (S * 4) + +#define NBR_AVG_SAMPLES 20 + +#define LOW_BAT_CHECK_INTERVAL (HZ / 16) /* 62.5 ms */ + +#define VALID_CAPACITY_SEC (45 * 60) /* 45 minutes */ +#define BATT_OK_MIN 2360 /* mV */ +#define BATT_OK_INCREMENT 50 /* mV */ +#define BATT_OK_MAX_NR_INCREMENTS 0xE + +/* FG constants */ +#define BATT_OVV 0x01 + +#define interpolate(x, x1, y1, x2, y2) \ + ((y1) + ((((y2) - (y1)) * ((x) - (x1))) / ((x2) - (x1)))); + +/** + * struct ab8500_fg_interrupts - ab8500 fg interupts + * @name: name of the interrupt + * @isr function pointer to the isr + */ +struct ab8500_fg_interrupts { + char *name; + irqreturn_t (*isr)(int irq, void *data); +}; + +enum ab8500_fg_discharge_state { + AB8500_FG_DISCHARGE_INIT, + AB8500_FG_DISCHARGE_INITMEASURING, + AB8500_FG_DISCHARGE_INIT_RECOVERY, + AB8500_FG_DISCHARGE_RECOVERY, + AB8500_FG_DISCHARGE_READOUT_INIT, + AB8500_FG_DISCHARGE_READOUT, + AB8500_FG_DISCHARGE_WAKEUP, +}; + +static char *discharge_state[] = { + "DISCHARGE_INIT", + "DISCHARGE_INITMEASURING", + "DISCHARGE_INIT_RECOVERY", + "DISCHARGE_RECOVERY", + "DISCHARGE_READOUT_INIT", + "DISCHARGE_READOUT", + "DISCHARGE_WAKEUP", +}; + +enum ab8500_fg_charge_state { + AB8500_FG_CHARGE_INIT, + AB8500_FG_CHARGE_READOUT, +}; + +static char *charge_state[] = { + "CHARGE_INIT", + "CHARGE_READOUT", +}; + +enum ab8500_fg_calibration_state { + AB8500_FG_CALIB_INIT, + AB8500_FG_CALIB_WAIT, + AB8500_FG_CALIB_END, +}; + +struct ab8500_fg_avg_cap { + int avg; + int samples[NBR_AVG_SAMPLES]; + time64_t time_stamps[NBR_AVG_SAMPLES]; + int pos; + int nbr_samples; + int sum; +}; + +struct ab8500_fg_cap_scaling { + bool enable; + int cap_to_scale[2]; + int disable_cap_level; + int scaled_cap; +}; + +struct ab8500_fg_battery_capacity { + int max_mah_design; + int max_mah; + int mah; + int permille; + int level; + int prev_mah; + int prev_percent; + int prev_level; + int user_mah; + struct ab8500_fg_cap_scaling cap_scale; +}; + +struct ab8500_fg_flags { + bool fg_enabled; + bool conv_done; + bool charging; + bool fully_charged; + bool force_full; + bool low_bat_delay; + bool low_bat; + bool bat_ovv; + bool batt_unknown; + bool calibrate; + bool user_cap; + bool batt_id_received; +}; + +struct inst_curr_result_list { + struct list_head list; + int *result; +}; + +/** + * struct ab8500_fg - ab8500 FG device information + * @dev: Pointer to the structure device + * @node: a list of AB8500 FGs, hence prepared for reentrance + * @irq holds the CCEOC interrupt number + * @vbat: Battery voltage in mV + * @vbat_nom: Nominal battery voltage in mV + * @inst_curr: Instantenous battery current in mA + * @avg_curr: Average battery current in mA + * @bat_temp battery temperature + * @fg_samples: Number of samples used in the FG accumulation + * @accu_charge: Accumulated charge from the last conversion + * @recovery_cnt: Counter for recovery mode + * @high_curr_cnt: Counter for high current mode + * @init_cnt: Counter for init mode + * @low_bat_cnt Counter for number of consecutive low battery measures + * @nbr_cceoc_irq_cnt Counter for number of CCEOC irqs received since enabled + * @recovery_needed: Indicate if recovery is needed + * @high_curr_mode: Indicate if we're in high current mode + * @init_capacity: Indicate if initial capacity measuring should be done + * @turn_off_fg: True if fg was off before current measurement + * @calib_state State during offset calibration + * @discharge_state: Current discharge state + * @charge_state: Current charge state + * @ab8500_fg_started Completion struct used for the instant current start + * @ab8500_fg_complete Completion struct used for the instant current reading + * @flags: Structure for information about events triggered + * @bat_cap: Structure for battery capacity specific parameters + * @avg_cap: Average capacity filter + * @parent: Pointer to the struct ab8500 + * @main_bat_v: ADC channel for the main battery voltage + * @bm: Platform specific battery management information + * @fg_psy: Structure that holds the FG specific battery properties + * @fg_wq: Work queue for running the FG algorithm + * @fg_periodic_work: Work to run the FG algorithm periodically + * @fg_low_bat_work: Work to check low bat condition + * @fg_reinit_work Work used to reset and reinitialise the FG algorithm + * @fg_work: Work to run the FG algorithm instantly + * @fg_acc_cur_work: Work to read the FG accumulator + * @fg_check_hw_failure_work: Work for checking HW state + * @cc_lock: Mutex for locking the CC + * @fg_kobject: Structure of type kobject + */ +struct ab8500_fg { + struct device *dev; + struct list_head node; + int irq; + int vbat; + int vbat_nom; + int inst_curr; + int avg_curr; + int bat_temp; + int fg_samples; + int accu_charge; + int recovery_cnt; + int high_curr_cnt; + int init_cnt; + int low_bat_cnt; + int nbr_cceoc_irq_cnt; + bool recovery_needed; + bool high_curr_mode; + bool init_capacity; + bool turn_off_fg; + enum ab8500_fg_calibration_state calib_state; + enum ab8500_fg_discharge_state discharge_state; + enum ab8500_fg_charge_state charge_state; + struct completion ab8500_fg_started; + struct completion ab8500_fg_complete; + struct ab8500_fg_flags flags; + struct ab8500_fg_battery_capacity bat_cap; + struct ab8500_fg_avg_cap avg_cap; + struct ab8500 *parent; + struct iio_channel *main_bat_v; + struct abx500_bm_data *bm; + struct power_supply *fg_psy; + struct workqueue_struct *fg_wq; + struct delayed_work fg_periodic_work; + struct delayed_work fg_low_bat_work; + struct delayed_work fg_reinit_work; + struct work_struct fg_work; + struct work_struct fg_acc_cur_work; + struct delayed_work fg_check_hw_failure_work; + struct mutex cc_lock; + struct kobject fg_kobject; +}; +static LIST_HEAD(ab8500_fg_list); + +/** + * ab8500_fg_get() - returns a reference to the primary AB8500 fuel gauge + * (i.e. the first fuel gauge in the instance list) + */ +struct ab8500_fg *ab8500_fg_get(void) +{ + return list_first_entry_or_null(&ab8500_fg_list, struct ab8500_fg, + node); +} + +/* Main battery properties */ +static enum power_supply_property ab8500_fg_props[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, + POWER_SUPPLY_PROP_ENERGY_FULL, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, +}; + +/* + * This array maps the raw hex value to lowbat voltage used by the AB8500 + * Values taken from the UM0836 + */ +static int ab8500_fg_lowbat_voltage_map[] = { + 2300 , + 2325 , + 2350 , + 2375 , + 2400 , + 2425 , + 2450 , + 2475 , + 2500 , + 2525 , + 2550 , + 2575 , + 2600 , + 2625 , + 2650 , + 2675 , + 2700 , + 2725 , + 2750 , + 2775 , + 2800 , + 2825 , + 2850 , + 2875 , + 2900 , + 2925 , + 2950 , + 2975 , + 3000 , + 3025 , + 3050 , + 3075 , + 3100 , + 3125 , + 3150 , + 3175 , + 3200 , + 3225 , + 3250 , + 3275 , + 3300 , + 3325 , + 3350 , + 3375 , + 3400 , + 3425 , + 3450 , + 3475 , + 3500 , + 3525 , + 3550 , + 3575 , + 3600 , + 3625 , + 3650 , + 3675 , + 3700 , + 3725 , + 3750 , + 3775 , + 3800 , + 3825 , + 3850 , + 3850 , +}; + +static u8 ab8500_volt_to_regval(int voltage) +{ + int i; + + if (voltage < ab8500_fg_lowbat_voltage_map[0]) + return 0; + + for (i = 0; i < ARRAY_SIZE(ab8500_fg_lowbat_voltage_map); i++) { + if (voltage < ab8500_fg_lowbat_voltage_map[i]) + return (u8) i - 1; + } + + /* If not captured above, return index of last element */ + return (u8) ARRAY_SIZE(ab8500_fg_lowbat_voltage_map) - 1; +} + +/** + * ab8500_fg_is_low_curr() - Low or high current mode + * @di: pointer to the ab8500_fg structure + * @curr: the current to base or our decision on + * + * Low current mode if the current consumption is below a certain threshold + */ +static int ab8500_fg_is_low_curr(struct ab8500_fg *di, int curr) +{ + /* + * We want to know if we're in low current mode + */ + if (curr > -di->bm->fg_params->high_curr_threshold) + return true; + else + return false; +} + +/** + * ab8500_fg_add_cap_sample() - Add capacity to average filter + * @di: pointer to the ab8500_fg structure + * @sample: the capacity in mAh to add to the filter + * + * A capacity is added to the filter and a new mean capacity is calculated and + * returned + */ +static int ab8500_fg_add_cap_sample(struct ab8500_fg *di, int sample) +{ + time64_t now = ktime_get_boottime_seconds(); + struct ab8500_fg_avg_cap *avg = &di->avg_cap; + + do { + avg->sum += sample - avg->samples[avg->pos]; + avg->samples[avg->pos] = sample; + avg->time_stamps[avg->pos] = now; + avg->pos++; + + if (avg->pos == NBR_AVG_SAMPLES) + avg->pos = 0; + + if (avg->nbr_samples < NBR_AVG_SAMPLES) + avg->nbr_samples++; + + /* + * Check the time stamp for each sample. If too old, + * replace with latest sample + */ + } while (now - VALID_CAPACITY_SEC > avg->time_stamps[avg->pos]); + + avg->avg = avg->sum / avg->nbr_samples; + + return avg->avg; +} + +/** + * ab8500_fg_clear_cap_samples() - Clear average filter + * @di: pointer to the ab8500_fg structure + * + * The capacity filter is is reset to zero. + */ +static void ab8500_fg_clear_cap_samples(struct ab8500_fg *di) +{ + int i; + struct ab8500_fg_avg_cap *avg = &di->avg_cap; + + avg->pos = 0; + avg->nbr_samples = 0; + avg->sum = 0; + avg->avg = 0; + + for (i = 0; i < NBR_AVG_SAMPLES; i++) { + avg->samples[i] = 0; + avg->time_stamps[i] = 0; + } +} + +/** + * ab8500_fg_fill_cap_sample() - Fill average filter + * @di: pointer to the ab8500_fg structure + * @sample: the capacity in mAh to fill the filter with + * + * The capacity filter is filled with a capacity in mAh + */ +static void ab8500_fg_fill_cap_sample(struct ab8500_fg *di, int sample) +{ + int i; + time64_t now; + struct ab8500_fg_avg_cap *avg = &di->avg_cap; + + now = ktime_get_boottime_seconds(); + + for (i = 0; i < NBR_AVG_SAMPLES; i++) { + avg->samples[i] = sample; + avg->time_stamps[i] = now; + } + + avg->pos = 0; + avg->nbr_samples = NBR_AVG_SAMPLES; + avg->sum = sample * NBR_AVG_SAMPLES; + avg->avg = sample; +} + +/** + * ab8500_fg_coulomb_counter() - enable coulomb counter + * @di: pointer to the ab8500_fg structure + * @enable: enable/disable + * + * Enable/Disable coulomb counter. + * On failure returns negative value. + */ +static int ab8500_fg_coulomb_counter(struct ab8500_fg *di, bool enable) +{ + int ret = 0; + mutex_lock(&di->cc_lock); + if (enable) { + /* To be able to reprogram the number of samples, we have to + * first stop the CC and then enable it again */ + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, 0x00); + if (ret) + goto cc_err; + + /* Program the samples */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_NCOV_ACCU, + di->fg_samples); + if (ret) + goto cc_err; + + /* Start the CC */ + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, + (CC_DEEP_SLEEP_ENA | CC_PWR_UP_ENA)); + if (ret) + goto cc_err; + + di->flags.fg_enabled = true; + } else { + /* Clear any pending read requests */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, + (RESET_ACCU | READ_REQ), 0); + if (ret) + goto cc_err; + + ret = abx500_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_NCOV_ACCU_CTRL, 0); + if (ret) + goto cc_err; + + /* Stop the CC */ + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, 0); + if (ret) + goto cc_err; + + di->flags.fg_enabled = false; + + } + dev_dbg(di->dev, " CC enabled: %d Samples: %d\n", + enable, di->fg_samples); + + mutex_unlock(&di->cc_lock); + + return ret; +cc_err: + dev_err(di->dev, "%s Enabling coulomb counter failed\n", __func__); + mutex_unlock(&di->cc_lock); + return ret; +} + +/** + * ab8500_fg_inst_curr_start() - start battery instantaneous current + * @di: pointer to the ab8500_fg structure + * + * Returns 0 or error code + * Note: This is part "one" and has to be called before + * ab8500_fg_inst_curr_finalize() + */ +int ab8500_fg_inst_curr_start(struct ab8500_fg *di) +{ + u8 reg_val; + int ret; + + mutex_lock(&di->cc_lock); + + di->nbr_cceoc_irq_cnt = 0; + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, ®_val); + if (ret < 0) + goto fail; + + if (!(reg_val & CC_PWR_UP_ENA)) { + dev_dbg(di->dev, "%s Enable FG\n", __func__); + di->turn_off_fg = true; + + /* Program the samples */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_NCOV_ACCU, + SEC_TO_SAMPLE(10)); + if (ret) + goto fail; + + /* Start the CC */ + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, + (CC_DEEP_SLEEP_ENA | CC_PWR_UP_ENA)); + if (ret) + goto fail; + } else { + di->turn_off_fg = false; + } + + /* Return and WFI */ + reinit_completion(&di->ab8500_fg_started); + reinit_completion(&di->ab8500_fg_complete); + enable_irq(di->irq); + + /* Note: cc_lock is still locked */ + return 0; +fail: + mutex_unlock(&di->cc_lock); + return ret; +} + +/** + * ab8500_fg_inst_curr_started() - check if fg conversion has started + * @di: pointer to the ab8500_fg structure + * + * Returns 1 if conversion started, 0 if still waiting + */ +int ab8500_fg_inst_curr_started(struct ab8500_fg *di) +{ + return completion_done(&di->ab8500_fg_started); +} + +/** + * ab8500_fg_inst_curr_done() - check if fg conversion is done + * @di: pointer to the ab8500_fg structure + * + * Returns 1 if conversion done, 0 if still waiting + */ +int ab8500_fg_inst_curr_done(struct ab8500_fg *di) +{ + return completion_done(&di->ab8500_fg_complete); +} + +/** + * ab8500_fg_inst_curr_finalize() - battery instantaneous current + * @di: pointer to the ab8500_fg structure + * @res: battery instantenous current(on success) + * + * Returns 0 or an error code + * Note: This is part "two" and has to be called at earliest 250 ms + * after ab8500_fg_inst_curr_start() + */ +int ab8500_fg_inst_curr_finalize(struct ab8500_fg *di, int *res) +{ + u8 low, high; + int val; + int ret; + unsigned long timeout; + + if (!completion_done(&di->ab8500_fg_complete)) { + timeout = wait_for_completion_timeout( + &di->ab8500_fg_complete, + INS_CURR_TIMEOUT); + dev_dbg(di->dev, "Finalize time: %d ms\n", + jiffies_to_msecs(INS_CURR_TIMEOUT - timeout)); + if (!timeout) { + ret = -ETIME; + disable_irq(di->irq); + di->nbr_cceoc_irq_cnt = 0; + dev_err(di->dev, "completion timed out [%d]\n", + __LINE__); + goto fail; + } + } + + disable_irq(di->irq); + di->nbr_cceoc_irq_cnt = 0; + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, + READ_REQ, READ_REQ); + + /* 100uS between read request and read is needed */ + usleep_range(100, 100); + + /* Read CC Sample conversion value Low and high */ + ret = abx500_get_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_SMPL_CNVL_REG, &low); + if (ret < 0) + goto fail; + + ret = abx500_get_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_SMPL_CNVH_REG, &high); + if (ret < 0) + goto fail; + + /* + * negative value for Discharging + * convert 2's complement into decimal + */ + if (high & 0x10) + val = (low | (high << 8) | 0xFFFFE000); + else + val = (low | (high << 8)); + + /* + * Convert to unit value in mA + * Full scale input voltage is + * 63.160mV => LSB = 63.160mV/(4096*res) = 1.542mA + * Given a 250ms conversion cycle time the LSB corresponds + * to 107.1 nAh. Convert to current by dividing by the conversion + * time in hours (250ms = 1 / (3600 * 4)h) + * 107.1nAh assumes 10mOhm, but fg_res is in 0.1mOhm + */ + val = (val * QLSB_NANO_AMP_HOURS_X10 * 36 * 4) / + (1000 * di->bm->fg_res); + + if (di->turn_off_fg) { + dev_dbg(di->dev, "%s Disable FG\n", __func__); + + /* Clear any pending read requests */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, 0); + if (ret) + goto fail; + + /* Stop the CC */ + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8500_RTC_CC_CONF_REG, 0); + if (ret) + goto fail; + } + mutex_unlock(&di->cc_lock); + (*res) = val; + + return 0; +fail: + mutex_unlock(&di->cc_lock); + return ret; +} + +/** + * ab8500_fg_inst_curr_blocking() - battery instantaneous current + * @di: pointer to the ab8500_fg structure + * @res: battery instantenous current(on success) + * + * Returns 0 else error code + */ +int ab8500_fg_inst_curr_blocking(struct ab8500_fg *di) +{ + int ret; + unsigned long timeout; + int res = 0; + + ret = ab8500_fg_inst_curr_start(di); + if (ret) { + dev_err(di->dev, "Failed to initialize fg_inst\n"); + return 0; + } + + /* Wait for CC to actually start */ + if (!completion_done(&di->ab8500_fg_started)) { + timeout = wait_for_completion_timeout( + &di->ab8500_fg_started, + INS_CURR_TIMEOUT); + dev_dbg(di->dev, "Start time: %d ms\n", + jiffies_to_msecs(INS_CURR_TIMEOUT - timeout)); + if (!timeout) { + ret = -ETIME; + dev_err(di->dev, "completion timed out [%d]\n", + __LINE__); + goto fail; + } + } + + ret = ab8500_fg_inst_curr_finalize(di, &res); + if (ret) { + dev_err(di->dev, "Failed to finalize fg_inst\n"); + return 0; + } + + dev_dbg(di->dev, "%s instant current: %d", __func__, res); + return res; +fail: + disable_irq(di->irq); + mutex_unlock(&di->cc_lock); + return ret; +} + +/** + * ab8500_fg_acc_cur_work() - average battery current + * @work: pointer to the work_struct structure + * + * Updated the average battery current obtained from the + * coulomb counter. + */ +static void ab8500_fg_acc_cur_work(struct work_struct *work) +{ + int val; + int ret; + u8 low, med, high; + + struct ab8500_fg *di = container_of(work, + struct ab8500_fg, fg_acc_cur_work); + + mutex_lock(&di->cc_lock); + ret = abx500_set_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_NCOV_ACCU_CTRL, RD_NCONV_ACCU_REQ); + if (ret) + goto exit; + + ret = abx500_get_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_NCOV_ACCU_LOW, &low); + if (ret < 0) + goto exit; + + ret = abx500_get_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_NCOV_ACCU_MED, &med); + if (ret < 0) + goto exit; + + ret = abx500_get_register_interruptible(di->dev, AB8500_GAS_GAUGE, + AB8500_GASG_CC_NCOV_ACCU_HIGH, &high); + if (ret < 0) + goto exit; + + /* Check for sign bit in case of negative value, 2's complement */ + if (high & 0x10) + val = (low | (med << 8) | (high << 16) | 0xFFE00000); + else + val = (low | (med << 8) | (high << 16)); + + /* + * Convert to uAh + * Given a 250ms conversion cycle time the LSB corresponds + * to 112.9 nAh. + * 112.9nAh assumes 10mOhm, but fg_res is in 0.1mOhm + */ + di->accu_charge = (val * QLSB_NANO_AMP_HOURS_X10) / + (100 * di->bm->fg_res); + + /* + * Convert to unit value in mA + * by dividing by the conversion + * time in hours (= samples / (3600 * 4)h) + * and multiply with 1000 + */ + di->avg_curr = (val * QLSB_NANO_AMP_HOURS_X10 * 36) / + (1000 * di->bm->fg_res * (di->fg_samples / 4)); + + di->flags.conv_done = true; + + mutex_unlock(&di->cc_lock); + + queue_work(di->fg_wq, &di->fg_work); + + dev_dbg(di->dev, "fg_res: %d, fg_samples: %d, gasg: %d, accu_charge: %d \n", + di->bm->fg_res, di->fg_samples, val, di->accu_charge); + return; +exit: + dev_err(di->dev, + "Failed to read or write gas gauge registers\n"); + mutex_unlock(&di->cc_lock); + queue_work(di->fg_wq, &di->fg_work); +} + +/** + * ab8500_fg_bat_voltage() - get battery voltage + * @di: pointer to the ab8500_fg structure + * + * Returns battery voltage(on success) else error code + */ +static int ab8500_fg_bat_voltage(struct ab8500_fg *di) +{ + int vbat, ret; + static int prev; + + ret = iio_read_channel_processed(di->main_bat_v, &vbat); + if (ret < 0) { + dev_err(di->dev, + "%s ADC conversion failed, using previous value\n", + __func__); + return prev; + } + + prev = vbat; + return vbat; +} + +/** + * ab8500_fg_volt_to_capacity() - Voltage based capacity + * @di: pointer to the ab8500_fg structure + * @voltage: The voltage to convert to a capacity + * + * Returns battery capacity in per mille based on voltage + */ +static int ab8500_fg_volt_to_capacity(struct ab8500_fg *di, int voltage) +{ + int i, tbl_size; + const struct abx500_v_to_cap *tbl; + int cap = 0; + + tbl = di->bm->bat_type[di->bm->batt_id].v_to_cap_tbl, + tbl_size = di->bm->bat_type[di->bm->batt_id].n_v_cap_tbl_elements; + + for (i = 0; i < tbl_size; ++i) { + if (voltage > tbl[i].voltage) + break; + } + + if ((i > 0) && (i < tbl_size)) { + cap = interpolate(voltage, + tbl[i].voltage, + tbl[i].capacity * 10, + tbl[i-1].voltage, + tbl[i-1].capacity * 10); + } else if (i == 0) { + cap = 1000; + } else { + cap = 0; + } + + dev_dbg(di->dev, "%s Vbat: %d, Cap: %d per mille", + __func__, voltage, cap); + + return cap; +} + +/** + * ab8500_fg_uncomp_volt_to_capacity() - Uncompensated voltage based capacity + * @di: pointer to the ab8500_fg structure + * + * Returns battery capacity based on battery voltage that is not compensated + * for the voltage drop due to the load + */ +static int ab8500_fg_uncomp_volt_to_capacity(struct ab8500_fg *di) +{ + di->vbat = ab8500_fg_bat_voltage(di); + return ab8500_fg_volt_to_capacity(di, di->vbat); +} + +/** + * ab8500_fg_battery_resistance() - Returns the battery inner resistance + * @di: pointer to the ab8500_fg structure + * + * Returns battery inner resistance added with the fuel gauge resistor value + * to get the total resistance in the whole link from gnd to bat+ node. + */ +static int ab8500_fg_battery_resistance(struct ab8500_fg *di) +{ + int i, tbl_size; + const struct batres_vs_temp *tbl; + int resist = 0; + + tbl = di->bm->bat_type[di->bm->batt_id].batres_tbl; + tbl_size = di->bm->bat_type[di->bm->batt_id].n_batres_tbl_elements; + + for (i = 0; i < tbl_size; ++i) { + if (di->bat_temp / 10 > tbl[i].temp) + break; + } + + if ((i > 0) && (i < tbl_size)) { + resist = interpolate(di->bat_temp / 10, + tbl[i].temp, + tbl[i].resist, + tbl[i-1].temp, + tbl[i-1].resist); + } else if (i == 0) { + resist = tbl[0].resist; + } else { + resist = tbl[tbl_size - 1].resist; + } + + dev_dbg(di->dev, "%s Temp: %d battery internal resistance: %d" + " fg resistance %d, total: %d (mOhm)\n", + __func__, di->bat_temp, resist, di->bm->fg_res / 10, + (di->bm->fg_res / 10) + resist); + + /* fg_res variable is in 0.1mOhm */ + resist += di->bm->fg_res / 10; + + return resist; +} + +/** + * ab8500_fg_load_comp_volt_to_capacity() - Load compensated voltage based capacity + * @di: pointer to the ab8500_fg structure + * + * Returns battery capacity based on battery voltage that is load compensated + * for the voltage drop + */ +static int ab8500_fg_load_comp_volt_to_capacity(struct ab8500_fg *di) +{ + int vbat_comp, res; + int i = 0; + int vbat = 0; + + ab8500_fg_inst_curr_start(di); + + do { + vbat += ab8500_fg_bat_voltage(di); + i++; + usleep_range(5000, 6000); + } while (!ab8500_fg_inst_curr_done(di)); + + ab8500_fg_inst_curr_finalize(di, &di->inst_curr); + + di->vbat = vbat / i; + res = ab8500_fg_battery_resistance(di); + + /* Use Ohms law to get the load compensated voltage */ + vbat_comp = di->vbat - (di->inst_curr * res) / 1000; + + dev_dbg(di->dev, "%s Measured Vbat: %dmV,Compensated Vbat %dmV, " + "R: %dmOhm, Current: %dmA Vbat Samples: %d\n", + __func__, di->vbat, vbat_comp, res, di->inst_curr, i); + + return ab8500_fg_volt_to_capacity(di, vbat_comp); +} + +/** + * ab8500_fg_convert_mah_to_permille() - Capacity in mAh to permille + * @di: pointer to the ab8500_fg structure + * @cap_mah: capacity in mAh + * + * Converts capacity in mAh to capacity in permille + */ +static int ab8500_fg_convert_mah_to_permille(struct ab8500_fg *di, int cap_mah) +{ + return (cap_mah * 1000) / di->bat_cap.max_mah_design; +} + +/** + * ab8500_fg_convert_permille_to_mah() - Capacity in permille to mAh + * @di: pointer to the ab8500_fg structure + * @cap_pm: capacity in permille + * + * Converts capacity in permille to capacity in mAh + */ +static int ab8500_fg_convert_permille_to_mah(struct ab8500_fg *di, int cap_pm) +{ + return cap_pm * di->bat_cap.max_mah_design / 1000; +} + +/** + * ab8500_fg_convert_mah_to_uwh() - Capacity in mAh to uWh + * @di: pointer to the ab8500_fg structure + * @cap_mah: capacity in mAh + * + * Converts capacity in mAh to capacity in uWh + */ +static int ab8500_fg_convert_mah_to_uwh(struct ab8500_fg *di, int cap_mah) +{ + u64 div_res; + u32 div_rem; + + div_res = ((u64) cap_mah) * ((u64) di->vbat_nom); + div_rem = do_div(div_res, 1000); + + /* Make sure to round upwards if necessary */ + if (div_rem >= 1000 / 2) + div_res++; + + return (int) div_res; +} + +/** + * ab8500_fg_calc_cap_charging() - Calculate remaining capacity while charging + * @di: pointer to the ab8500_fg structure + * + * Return the capacity in mAh based on previous calculated capcity and the FG + * accumulator register value. The filter is filled with this capacity + */ +static int ab8500_fg_calc_cap_charging(struct ab8500_fg *di) +{ + dev_dbg(di->dev, "%s cap_mah %d accu_charge %d\n", + __func__, + di->bat_cap.mah, + di->accu_charge); + + /* Capacity should not be less than 0 */ + if (di->bat_cap.mah + di->accu_charge > 0) + di->bat_cap.mah += di->accu_charge; + else + di->bat_cap.mah = 0; + /* + * We force capacity to 100% once when the algorithm + * reports that it's full. + */ + if (di->bat_cap.mah >= di->bat_cap.max_mah_design || + di->flags.force_full) { + di->bat_cap.mah = di->bat_cap.max_mah_design; + } + + ab8500_fg_fill_cap_sample(di, di->bat_cap.mah); + di->bat_cap.permille = + ab8500_fg_convert_mah_to_permille(di, di->bat_cap.mah); + + /* We need to update battery voltage and inst current when charging */ + di->vbat = ab8500_fg_bat_voltage(di); + di->inst_curr = ab8500_fg_inst_curr_blocking(di); + + return di->bat_cap.mah; +} + +/** + * ab8500_fg_calc_cap_discharge_voltage() - Capacity in discharge with voltage + * @di: pointer to the ab8500_fg structure + * @comp: if voltage should be load compensated before capacity calc + * + * Return the capacity in mAh based on the battery voltage. The voltage can + * either be load compensated or not. This value is added to the filter and a + * new mean value is calculated and returned. + */ +static int ab8500_fg_calc_cap_discharge_voltage(struct ab8500_fg *di, bool comp) +{ + int permille, mah; + + if (comp) + permille = ab8500_fg_load_comp_volt_to_capacity(di); + else + permille = ab8500_fg_uncomp_volt_to_capacity(di); + + mah = ab8500_fg_convert_permille_to_mah(di, permille); + + di->bat_cap.mah = ab8500_fg_add_cap_sample(di, mah); + di->bat_cap.permille = + ab8500_fg_convert_mah_to_permille(di, di->bat_cap.mah); + + return di->bat_cap.mah; +} + +/** + * ab8500_fg_calc_cap_discharge_fg() - Capacity in discharge with FG + * @di: pointer to the ab8500_fg structure + * + * Return the capacity in mAh based on previous calculated capcity and the FG + * accumulator register value. This value is added to the filter and a + * new mean value is calculated and returned. + */ +static int ab8500_fg_calc_cap_discharge_fg(struct ab8500_fg *di) +{ + int permille_volt, permille; + + dev_dbg(di->dev, "%s cap_mah %d accu_charge %d\n", + __func__, + di->bat_cap.mah, + di->accu_charge); + + /* Capacity should not be less than 0 */ + if (di->bat_cap.mah + di->accu_charge > 0) + di->bat_cap.mah += di->accu_charge; + else + di->bat_cap.mah = 0; + + if (di->bat_cap.mah >= di->bat_cap.max_mah_design) + di->bat_cap.mah = di->bat_cap.max_mah_design; + + /* + * Check against voltage based capacity. It can not be lower + * than what the uncompensated voltage says + */ + permille = ab8500_fg_convert_mah_to_permille(di, di->bat_cap.mah); + permille_volt = ab8500_fg_uncomp_volt_to_capacity(di); + + if (permille < permille_volt) { + di->bat_cap.permille = permille_volt; + di->bat_cap.mah = ab8500_fg_convert_permille_to_mah(di, + di->bat_cap.permille); + + dev_dbg(di->dev, "%s voltage based: perm %d perm_volt %d\n", + __func__, + permille, + permille_volt); + + ab8500_fg_fill_cap_sample(di, di->bat_cap.mah); + } else { + ab8500_fg_fill_cap_sample(di, di->bat_cap.mah); + di->bat_cap.permille = + ab8500_fg_convert_mah_to_permille(di, di->bat_cap.mah); + } + + return di->bat_cap.mah; +} + +/** + * ab8500_fg_capacity_level() - Get the battery capacity level + * @di: pointer to the ab8500_fg structure + * + * Get the battery capacity level based on the capacity in percent + */ +static int ab8500_fg_capacity_level(struct ab8500_fg *di) +{ + int ret, percent; + + percent = DIV_ROUND_CLOSEST(di->bat_cap.permille, 10); + + if (percent <= di->bm->cap_levels->critical || + di->flags.low_bat) + ret = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else if (percent <= di->bm->cap_levels->low) + ret = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (percent <= di->bm->cap_levels->normal) + ret = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + else if (percent <= di->bm->cap_levels->high) + ret = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else + ret = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + + return ret; +} + +/** + * ab8500_fg_calculate_scaled_capacity() - Capacity scaling + * @di: pointer to the ab8500_fg structure + * + * Calculates the capacity to be shown to upper layers. Scales the capacity + * to have 100% as a reference from the actual capacity upon removal of charger + * when charging is in maintenance mode. + */ +static int ab8500_fg_calculate_scaled_capacity(struct ab8500_fg *di) +{ + struct ab8500_fg_cap_scaling *cs = &di->bat_cap.cap_scale; + int capacity = di->bat_cap.prev_percent; + + if (!cs->enable) + return capacity; + + /* + * As long as we are in fully charge mode scale the capacity + * to show 100%. + */ + if (di->flags.fully_charged) { + cs->cap_to_scale[0] = 100; + cs->cap_to_scale[1] = + max(capacity, di->bm->fg_params->maint_thres); + dev_dbg(di->dev, "Scale cap with %d/%d\n", + cs->cap_to_scale[0], cs->cap_to_scale[1]); + } + + /* Calculates the scaled capacity. */ + if ((cs->cap_to_scale[0] != cs->cap_to_scale[1]) + && (cs->cap_to_scale[1] > 0)) + capacity = min(100, + DIV_ROUND_CLOSEST(di->bat_cap.prev_percent * + cs->cap_to_scale[0], + cs->cap_to_scale[1])); + + if (di->flags.charging) { + if (capacity < cs->disable_cap_level) { + cs->disable_cap_level = capacity; + dev_dbg(di->dev, "Cap to stop scale lowered %d%%\n", + cs->disable_cap_level); + } else if (!di->flags.fully_charged) { + if (di->bat_cap.prev_percent >= + cs->disable_cap_level) { + dev_dbg(di->dev, "Disabling scaled capacity\n"); + cs->enable = false; + capacity = di->bat_cap.prev_percent; + } else { + dev_dbg(di->dev, + "Waiting in cap to level %d%%\n", + cs->disable_cap_level); + capacity = cs->disable_cap_level; + } + } + } + + return capacity; +} + +/** + * ab8500_fg_update_cap_scalers() - Capacity scaling + * @di: pointer to the ab8500_fg structure + * + * To be called when state change from charge<->discharge to update + * the capacity scalers. + */ +static void ab8500_fg_update_cap_scalers(struct ab8500_fg *di) +{ + struct ab8500_fg_cap_scaling *cs = &di->bat_cap.cap_scale; + + if (!cs->enable) + return; + if (di->flags.charging) { + di->bat_cap.cap_scale.disable_cap_level = + di->bat_cap.cap_scale.scaled_cap; + dev_dbg(di->dev, "Cap to stop scale at charge %d%%\n", + di->bat_cap.cap_scale.disable_cap_level); + } else { + if (cs->scaled_cap != 100) { + cs->cap_to_scale[0] = cs->scaled_cap; + cs->cap_to_scale[1] = di->bat_cap.prev_percent; + } else { + cs->cap_to_scale[0] = 100; + cs->cap_to_scale[1] = + max(di->bat_cap.prev_percent, + di->bm->fg_params->maint_thres); + } + + dev_dbg(di->dev, "Cap to scale at discharge %d/%d\n", + cs->cap_to_scale[0], cs->cap_to_scale[1]); + } +} + +/** + * ab8500_fg_check_capacity_limits() - Check if capacity has changed + * @di: pointer to the ab8500_fg structure + * @init: capacity is allowed to go up in init mode + * + * Check if capacity or capacity limit has changed and notify the system + * about it using the power_supply framework + */ +static void ab8500_fg_check_capacity_limits(struct ab8500_fg *di, bool init) +{ + bool changed = false; + int percent = DIV_ROUND_CLOSEST(di->bat_cap.permille, 10); + + di->bat_cap.level = ab8500_fg_capacity_level(di); + + if (di->bat_cap.level != di->bat_cap.prev_level) { + /* + * We do not allow reported capacity level to go up + * unless we're charging or if we're in init + */ + if (!(!di->flags.charging && di->bat_cap.level > + di->bat_cap.prev_level) || init) { + dev_dbg(di->dev, "level changed from %d to %d\n", + di->bat_cap.prev_level, + di->bat_cap.level); + di->bat_cap.prev_level = di->bat_cap.level; + changed = true; + } else { + dev_dbg(di->dev, "level not allowed to go up " + "since no charger is connected: %d to %d\n", + di->bat_cap.prev_level, + di->bat_cap.level); + } + } + + /* + * If we have received the LOW_BAT IRQ, set capacity to 0 to initiate + * shutdown + */ + if (di->flags.low_bat) { + dev_dbg(di->dev, "Battery low, set capacity to 0\n"); + di->bat_cap.prev_percent = 0; + di->bat_cap.permille = 0; + percent = 0; + di->bat_cap.prev_mah = 0; + di->bat_cap.mah = 0; + changed = true; + } else if (di->flags.fully_charged) { + /* + * We report 100% if algorithm reported fully charged + * and show 100% during maintenance charging (scaling). + */ + if (di->flags.force_full) { + di->bat_cap.prev_percent = percent; + di->bat_cap.prev_mah = di->bat_cap.mah; + + changed = true; + + if (!di->bat_cap.cap_scale.enable && + di->bm->capacity_scaling) { + di->bat_cap.cap_scale.enable = true; + di->bat_cap.cap_scale.cap_to_scale[0] = 100; + di->bat_cap.cap_scale.cap_to_scale[1] = + di->bat_cap.prev_percent; + di->bat_cap.cap_scale.disable_cap_level = 100; + } + } else if (di->bat_cap.prev_percent != percent) { + dev_dbg(di->dev, + "battery reported full " + "but capacity dropping: %d\n", + percent); + di->bat_cap.prev_percent = percent; + di->bat_cap.prev_mah = di->bat_cap.mah; + + changed = true; + } + } else if (di->bat_cap.prev_percent != percent) { + if (percent == 0) { + /* + * We will not report 0% unless we've got + * the LOW_BAT IRQ, no matter what the FG + * algorithm says. + */ + di->bat_cap.prev_percent = 1; + percent = 1; + + changed = true; + } else if (!(!di->flags.charging && + percent > di->bat_cap.prev_percent) || init) { + /* + * We do not allow reported capacity to go up + * unless we're charging or if we're in init + */ + dev_dbg(di->dev, + "capacity changed from %d to %d (%d)\n", + di->bat_cap.prev_percent, + percent, + di->bat_cap.permille); + di->bat_cap.prev_percent = percent; + di->bat_cap.prev_mah = di->bat_cap.mah; + + changed = true; + } else { + dev_dbg(di->dev, "capacity not allowed to go up since " + "no charger is connected: %d to %d (%d)\n", + di->bat_cap.prev_percent, + percent, + di->bat_cap.permille); + } + } + + if (changed) { + if (di->bm->capacity_scaling) { + di->bat_cap.cap_scale.scaled_cap = + ab8500_fg_calculate_scaled_capacity(di); + + dev_info(di->dev, "capacity=%d (%d)\n", + di->bat_cap.prev_percent, + di->bat_cap.cap_scale.scaled_cap); + } + power_supply_changed(di->fg_psy); + if (di->flags.fully_charged && di->flags.force_full) { + dev_dbg(di->dev, "Battery full, notifying.\n"); + di->flags.force_full = false; + sysfs_notify(&di->fg_kobject, NULL, "charge_full"); + } + sysfs_notify(&di->fg_kobject, NULL, "charge_now"); + } +} + +static void ab8500_fg_charge_state_to(struct ab8500_fg *di, + enum ab8500_fg_charge_state new_state) +{ + dev_dbg(di->dev, "Charge state from %d [%s] to %d [%s]\n", + di->charge_state, + charge_state[di->charge_state], + new_state, + charge_state[new_state]); + + di->charge_state = new_state; +} + +static void ab8500_fg_discharge_state_to(struct ab8500_fg *di, + enum ab8500_fg_discharge_state new_state) +{ + dev_dbg(di->dev, "Discharge state from %d [%s] to %d [%s]\n", + di->discharge_state, + discharge_state[di->discharge_state], + new_state, + discharge_state[new_state]); + + di->discharge_state = new_state; +} + +/** + * ab8500_fg_algorithm_charging() - FG algorithm for when charging + * @di: pointer to the ab8500_fg structure + * + * Battery capacity calculation state machine for when we're charging + */ +static void ab8500_fg_algorithm_charging(struct ab8500_fg *di) +{ + /* + * If we change to discharge mode + * we should start with recovery + */ + if (di->discharge_state != AB8500_FG_DISCHARGE_INIT_RECOVERY) + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_INIT_RECOVERY); + + switch (di->charge_state) { + case AB8500_FG_CHARGE_INIT: + di->fg_samples = SEC_TO_SAMPLE( + di->bm->fg_params->accu_charging); + + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_charge_state_to(di, AB8500_FG_CHARGE_READOUT); + + break; + + case AB8500_FG_CHARGE_READOUT: + /* + * Read the FG and calculate the new capacity + */ + mutex_lock(&di->cc_lock); + if (!di->flags.conv_done && !di->flags.force_full) { + /* Wasn't the CC IRQ that got us here */ + mutex_unlock(&di->cc_lock); + dev_dbg(di->dev, "%s CC conv not done\n", + __func__); + + break; + } + di->flags.conv_done = false; + mutex_unlock(&di->cc_lock); + + ab8500_fg_calc_cap_charging(di); + + break; + + default: + break; + } + + /* Check capacity limits */ + ab8500_fg_check_capacity_limits(di, false); +} + +static void force_capacity(struct ab8500_fg *di) +{ + int cap; + + ab8500_fg_clear_cap_samples(di); + cap = di->bat_cap.user_mah; + if (cap > di->bat_cap.max_mah_design) { + dev_dbg(di->dev, "Remaining cap %d can't be bigger than total" + " %d\n", cap, di->bat_cap.max_mah_design); + cap = di->bat_cap.max_mah_design; + } + ab8500_fg_fill_cap_sample(di, di->bat_cap.user_mah); + di->bat_cap.permille = ab8500_fg_convert_mah_to_permille(di, cap); + di->bat_cap.mah = cap; + ab8500_fg_check_capacity_limits(di, true); +} + +static bool check_sysfs_capacity(struct ab8500_fg *di) +{ + int cap, lower, upper; + int cap_permille; + + cap = di->bat_cap.user_mah; + + cap_permille = ab8500_fg_convert_mah_to_permille(di, + di->bat_cap.user_mah); + + lower = di->bat_cap.permille - di->bm->fg_params->user_cap_limit * 10; + upper = di->bat_cap.permille + di->bm->fg_params->user_cap_limit * 10; + + if (lower < 0) + lower = 0; + /* 1000 is permille, -> 100 percent */ + if (upper > 1000) + upper = 1000; + + dev_dbg(di->dev, "Capacity limits:" + " (Lower: %d User: %d Upper: %d) [user: %d, was: %d]\n", + lower, cap_permille, upper, cap, di->bat_cap.mah); + + /* If within limits, use the saved capacity and exit estimation...*/ + if (cap_permille > lower && cap_permille < upper) { + dev_dbg(di->dev, "OK! Using users cap %d uAh now\n", cap); + force_capacity(di); + return true; + } + dev_dbg(di->dev, "Capacity from user out of limits, ignoring"); + return false; +} + +/** + * ab8500_fg_algorithm_discharging() - FG algorithm for when discharging + * @di: pointer to the ab8500_fg structure + * + * Battery capacity calculation state machine for when we're discharging + */ +static void ab8500_fg_algorithm_discharging(struct ab8500_fg *di) +{ + int sleep_time; + + /* If we change to charge mode we should start with init */ + if (di->charge_state != AB8500_FG_CHARGE_INIT) + ab8500_fg_charge_state_to(di, AB8500_FG_CHARGE_INIT); + + switch (di->discharge_state) { + case AB8500_FG_DISCHARGE_INIT: + /* We use the FG IRQ to work on */ + di->init_cnt = 0; + di->fg_samples = SEC_TO_SAMPLE(di->bm->fg_params->init_timer); + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_INITMEASURING); + + fallthrough; + case AB8500_FG_DISCHARGE_INITMEASURING: + /* + * Discard a number of samples during startup. + * After that, use compensated voltage for a few + * samples to get an initial capacity. + * Then go to READOUT + */ + sleep_time = di->bm->fg_params->init_timer; + + /* Discard the first [x] seconds */ + if (di->init_cnt > di->bm->fg_params->init_discard_time) { + ab8500_fg_calc_cap_discharge_voltage(di, true); + + ab8500_fg_check_capacity_limits(di, true); + } + + di->init_cnt += sleep_time; + if (di->init_cnt > di->bm->fg_params->init_total_time) + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT_INIT); + + break; + + case AB8500_FG_DISCHARGE_INIT_RECOVERY: + di->recovery_cnt = 0; + di->recovery_needed = true; + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_RECOVERY); + + fallthrough; + + case AB8500_FG_DISCHARGE_RECOVERY: + sleep_time = di->bm->fg_params->recovery_sleep_timer; + + /* + * We should check the power consumption + * If low, go to READOUT (after x min) or + * RECOVERY_SLEEP if time left. + * If high, go to READOUT + */ + di->inst_curr = ab8500_fg_inst_curr_blocking(di); + + if (ab8500_fg_is_low_curr(di, di->inst_curr)) { + if (di->recovery_cnt > + di->bm->fg_params->recovery_total_time) { + di->fg_samples = SEC_TO_SAMPLE( + di->bm->fg_params->accu_high_curr); + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT); + di->recovery_needed = false; + } else { + queue_delayed_work(di->fg_wq, + &di->fg_periodic_work, + sleep_time * HZ); + } + di->recovery_cnt += sleep_time; + } else { + di->fg_samples = SEC_TO_SAMPLE( + di->bm->fg_params->accu_high_curr); + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT); + } + break; + + case AB8500_FG_DISCHARGE_READOUT_INIT: + di->fg_samples = SEC_TO_SAMPLE( + di->bm->fg_params->accu_high_curr); + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT); + break; + + case AB8500_FG_DISCHARGE_READOUT: + di->inst_curr = ab8500_fg_inst_curr_blocking(di); + + if (ab8500_fg_is_low_curr(di, di->inst_curr)) { + /* Detect mode change */ + if (di->high_curr_mode) { + di->high_curr_mode = false; + di->high_curr_cnt = 0; + } + + if (di->recovery_needed) { + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_INIT_RECOVERY); + + queue_delayed_work(di->fg_wq, + &di->fg_periodic_work, 0); + + break; + } + + ab8500_fg_calc_cap_discharge_voltage(di, true); + } else { + mutex_lock(&di->cc_lock); + if (!di->flags.conv_done) { + /* Wasn't the CC IRQ that got us here */ + mutex_unlock(&di->cc_lock); + dev_dbg(di->dev, "%s CC conv not done\n", + __func__); + + break; + } + di->flags.conv_done = false; + mutex_unlock(&di->cc_lock); + + /* Detect mode change */ + if (!di->high_curr_mode) { + di->high_curr_mode = true; + di->high_curr_cnt = 0; + } + + di->high_curr_cnt += + di->bm->fg_params->accu_high_curr; + if (di->high_curr_cnt > + di->bm->fg_params->high_curr_time) + di->recovery_needed = true; + + ab8500_fg_calc_cap_discharge_fg(di); + } + + ab8500_fg_check_capacity_limits(di, false); + + break; + + case AB8500_FG_DISCHARGE_WAKEUP: + ab8500_fg_calc_cap_discharge_voltage(di, true); + + di->fg_samples = SEC_TO_SAMPLE( + di->bm->fg_params->accu_high_curr); + ab8500_fg_coulomb_counter(di, true); + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT); + + ab8500_fg_check_capacity_limits(di, false); + + break; + + default: + break; + } +} + +/** + * ab8500_fg_algorithm_calibrate() - Internal columb counter offset calibration + * @di: pointer to the ab8500_fg structure + * + */ +static void ab8500_fg_algorithm_calibrate(struct ab8500_fg *di) +{ + int ret; + + switch (di->calib_state) { + case AB8500_FG_CALIB_INIT: + dev_dbg(di->dev, "Calibration ongoing...\n"); + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, + CC_INT_CAL_N_AVG_MASK, CC_INT_CAL_SAMPLES_8); + if (ret < 0) + goto err; + + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, + CC_INTAVGOFFSET_ENA, CC_INTAVGOFFSET_ENA); + if (ret < 0) + goto err; + di->calib_state = AB8500_FG_CALIB_WAIT; + break; + case AB8500_FG_CALIB_END: + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_GAS_GAUGE, AB8500_GASG_CC_CTRL_REG, + CC_MUXOFFSET, CC_MUXOFFSET); + if (ret < 0) + goto err; + di->flags.calibrate = false; + dev_dbg(di->dev, "Calibration done...\n"); + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + break; + case AB8500_FG_CALIB_WAIT: + dev_dbg(di->dev, "Calibration WFI\n"); + default: + break; + } + return; +err: + /* Something went wrong, don't calibrate then */ + dev_err(di->dev, "failed to calibrate the CC\n"); + di->flags.calibrate = false; + di->calib_state = AB8500_FG_CALIB_INIT; + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); +} + +/** + * ab8500_fg_algorithm() - Entry point for the FG algorithm + * @di: pointer to the ab8500_fg structure + * + * Entry point for the battery capacity calculation state machine + */ +static void ab8500_fg_algorithm(struct ab8500_fg *di) +{ + if (di->flags.calibrate) + ab8500_fg_algorithm_calibrate(di); + else { + if (di->flags.charging) + ab8500_fg_algorithm_charging(di); + else + ab8500_fg_algorithm_discharging(di); + } + + dev_dbg(di->dev, "[FG_DATA] %d %d %d %d %d %d %d %d %d %d " + "%d %d %d %d %d %d %d\n", + di->bat_cap.max_mah_design, + di->bat_cap.max_mah, + di->bat_cap.mah, + di->bat_cap.permille, + di->bat_cap.level, + di->bat_cap.prev_mah, + di->bat_cap.prev_percent, + di->bat_cap.prev_level, + di->vbat, + di->inst_curr, + di->avg_curr, + di->accu_charge, + di->flags.charging, + di->charge_state, + di->discharge_state, + di->high_curr_mode, + di->recovery_needed); +} + +/** + * ab8500_fg_periodic_work() - Run the FG state machine periodically + * @work: pointer to the work_struct structure + * + * Work queue function for periodic work + */ +static void ab8500_fg_periodic_work(struct work_struct *work) +{ + struct ab8500_fg *di = container_of(work, struct ab8500_fg, + fg_periodic_work.work); + + if (di->init_capacity) { + /* Get an initial capacity calculation */ + ab8500_fg_calc_cap_discharge_voltage(di, true); + ab8500_fg_check_capacity_limits(di, true); + di->init_capacity = false; + + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + } else if (di->flags.user_cap) { + if (check_sysfs_capacity(di)) { + ab8500_fg_check_capacity_limits(di, true); + if (di->flags.charging) + ab8500_fg_charge_state_to(di, + AB8500_FG_CHARGE_INIT); + else + ab8500_fg_discharge_state_to(di, + AB8500_FG_DISCHARGE_READOUT_INIT); + } + di->flags.user_cap = false; + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + } else + ab8500_fg_algorithm(di); + +} + +/** + * ab8500_fg_check_hw_failure_work() - Check OVV_BAT condition + * @work: pointer to the work_struct structure + * + * Work queue function for checking the OVV_BAT condition + */ +static void ab8500_fg_check_hw_failure_work(struct work_struct *work) +{ + int ret; + u8 reg_value; + + struct ab8500_fg *di = container_of(work, struct ab8500_fg, + fg_check_hw_failure_work.work); + + /* + * If we have had a battery over-voltage situation, + * check ovv-bit to see if it should be reset. + */ + ret = abx500_get_register_interruptible(di->dev, + AB8500_CHARGER, AB8500_CH_STAT_REG, + ®_value); + if (ret < 0) { + dev_err(di->dev, "%s ab8500 read failed\n", __func__); + return; + } + if ((reg_value & BATT_OVV) == BATT_OVV) { + if (!di->flags.bat_ovv) { + dev_dbg(di->dev, "Battery OVV\n"); + di->flags.bat_ovv = true; + power_supply_changed(di->fg_psy); + } + /* Not yet recovered from ovv, reschedule this test */ + queue_delayed_work(di->fg_wq, &di->fg_check_hw_failure_work, + HZ); + } else { + dev_dbg(di->dev, "Battery recovered from OVV\n"); + di->flags.bat_ovv = false; + power_supply_changed(di->fg_psy); + } +} + +/** + * ab8500_fg_low_bat_work() - Check LOW_BAT condition + * @work: pointer to the work_struct structure + * + * Work queue function for checking the LOW_BAT condition + */ +static void ab8500_fg_low_bat_work(struct work_struct *work) +{ + int vbat; + + struct ab8500_fg *di = container_of(work, struct ab8500_fg, + fg_low_bat_work.work); + + vbat = ab8500_fg_bat_voltage(di); + + /* Check if LOW_BAT still fulfilled */ + if (vbat < di->bm->fg_params->lowbat_threshold) { + /* Is it time to shut down? */ + if (di->low_bat_cnt < 1) { + di->flags.low_bat = true; + dev_warn(di->dev, "Shut down pending...\n"); + } else { + /* + * Else we need to re-schedule this check to be able to detect + * if the voltage increases again during charging or + * due to decreasing load. + */ + di->low_bat_cnt--; + dev_warn(di->dev, "Battery voltage still LOW\n"); + queue_delayed_work(di->fg_wq, &di->fg_low_bat_work, + round_jiffies(LOW_BAT_CHECK_INTERVAL)); + } + } else { + di->flags.low_bat_delay = false; + di->low_bat_cnt = 10; + dev_warn(di->dev, "Battery voltage OK again\n"); + } + + /* This is needed to dispatch LOW_BAT */ + ab8500_fg_check_capacity_limits(di, false); +} + +/** + * ab8500_fg_battok_calc - calculate the bit pattern corresponding + * to the target voltage. + * @di: pointer to the ab8500_fg structure + * @target: target voltage + * + * Returns bit pattern closest to the target voltage + * valid return values are 0-14. (0-BATT_OK_MAX_NR_INCREMENTS) + */ + +static int ab8500_fg_battok_calc(struct ab8500_fg *di, int target) +{ + if (target > BATT_OK_MIN + + (BATT_OK_INCREMENT * BATT_OK_MAX_NR_INCREMENTS)) + return BATT_OK_MAX_NR_INCREMENTS; + if (target < BATT_OK_MIN) + return 0; + return (target - BATT_OK_MIN) / BATT_OK_INCREMENT; +} + +/** + * ab8500_fg_battok_init_hw_register - init battok levels + * @di: pointer to the ab8500_fg structure + * + */ + +static int ab8500_fg_battok_init_hw_register(struct ab8500_fg *di) +{ + int selected; + int sel0; + int sel1; + int cbp_sel0; + int cbp_sel1; + int ret; + int new_val; + + sel0 = di->bm->fg_params->battok_falling_th_sel0; + sel1 = di->bm->fg_params->battok_raising_th_sel1; + + cbp_sel0 = ab8500_fg_battok_calc(di, sel0); + cbp_sel1 = ab8500_fg_battok_calc(di, sel1); + + selected = BATT_OK_MIN + cbp_sel0 * BATT_OK_INCREMENT; + + if (selected != sel0) + dev_warn(di->dev, "Invalid voltage step:%d, using %d %d\n", + sel0, selected, cbp_sel0); + + selected = BATT_OK_MIN + cbp_sel1 * BATT_OK_INCREMENT; + + if (selected != sel1) + dev_warn(di->dev, "Invalid voltage step:%d, using %d %d\n", + sel1, selected, cbp_sel1); + + new_val = cbp_sel0 | (cbp_sel1 << 4); + + dev_dbg(di->dev, "using: %x %d %d\n", new_val, cbp_sel0, cbp_sel1); + ret = abx500_set_register_interruptible(di->dev, AB8500_SYS_CTRL2_BLOCK, + AB8500_BATT_OK_REG, new_val); + return ret; +} + +/** + * ab8500_fg_instant_work() - Run the FG state machine instantly + * @work: pointer to the work_struct structure + * + * Work queue function for instant work + */ +static void ab8500_fg_instant_work(struct work_struct *work) +{ + struct ab8500_fg *di = container_of(work, struct ab8500_fg, fg_work); + + ab8500_fg_algorithm(di); +} + +/** + * ab8500_fg_cc_data_end_handler() - end of data conversion isr. + * @irq: interrupt number + * @_di: pointer to the ab8500_fg structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_fg_cc_data_end_handler(int irq, void *_di) +{ + struct ab8500_fg *di = _di; + if (!di->nbr_cceoc_irq_cnt) { + di->nbr_cceoc_irq_cnt++; + complete(&di->ab8500_fg_started); + } else { + di->nbr_cceoc_irq_cnt = 0; + complete(&di->ab8500_fg_complete); + } + return IRQ_HANDLED; +} + +/** + * ab8500_fg_cc_int_calib_handler () - end of calibration isr. + * @irq: interrupt number + * @_di: pointer to the ab8500_fg structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_fg_cc_int_calib_handler(int irq, void *_di) +{ + struct ab8500_fg *di = _di; + di->calib_state = AB8500_FG_CALIB_END; + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + return IRQ_HANDLED; +} + +/** + * ab8500_fg_cc_convend_handler() - isr to get battery avg current. + * @irq: interrupt number + * @_di: pointer to the ab8500_fg structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_fg_cc_convend_handler(int irq, void *_di) +{ + struct ab8500_fg *di = _di; + + queue_work(di->fg_wq, &di->fg_acc_cur_work); + + return IRQ_HANDLED; +} + +/** + * ab8500_fg_batt_ovv_handler() - Battery OVV occured + * @irq: interrupt number + * @_di: pointer to the ab8500_fg structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_fg_batt_ovv_handler(int irq, void *_di) +{ + struct ab8500_fg *di = _di; + + dev_dbg(di->dev, "Battery OVV\n"); + + /* Schedule a new HW failure check */ + queue_delayed_work(di->fg_wq, &di->fg_check_hw_failure_work, 0); + + return IRQ_HANDLED; +} + +/** + * ab8500_fg_lowbatf_handler() - Battery voltage is below LOW threshold + * @irq: interrupt number + * @_di: pointer to the ab8500_fg structure + * + * Returns IRQ status(IRQ_HANDLED) + */ +static irqreturn_t ab8500_fg_lowbatf_handler(int irq, void *_di) +{ + struct ab8500_fg *di = _di; + + /* Initiate handling in ab8500_fg_low_bat_work() if not already initiated. */ + if (!di->flags.low_bat_delay) { + dev_warn(di->dev, "Battery voltage is below LOW threshold\n"); + di->flags.low_bat_delay = true; + /* + * Start a timer to check LOW_BAT again after some time + * This is done to avoid shutdown on single voltage dips + */ + queue_delayed_work(di->fg_wq, &di->fg_low_bat_work, + round_jiffies(LOW_BAT_CHECK_INTERVAL)); + } + return IRQ_HANDLED; +} + +/** + * ab8500_fg_get_property() - get the fg properties + * @psy: pointer to the power_supply structure + * @psp: pointer to the power_supply_property structure + * @val: pointer to the power_supply_propval union + * + * This function gets called when an application tries to get the + * fg properties by reading the sysfs files. + * voltage_now: battery voltage + * current_now: battery instant current + * current_avg: battery average current + * charge_full_design: capacity where battery is considered full + * charge_now: battery capacity in nAh + * capacity: capacity in percent + * capacity_level: capacity level + * + * Returns error code in case of failure else 0 on success + */ +static int ab8500_fg_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + /* + * If battery is identified as unknown and charging of unknown + * batteries is disabled, we always report 100% capacity and + * capacity level UNKNOWN, since we can't calculate + * remaining capacity + */ + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (di->flags.bat_ovv) + val->intval = BATT_OVV_VALUE * 1000; + else + val->intval = di->vbat * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = di->inst_curr * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + val->intval = di->avg_curr * 1000; + break; + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + val->intval = ab8500_fg_convert_mah_to_uwh(di, + di->bat_cap.max_mah_design); + break; + case POWER_SUPPLY_PROP_ENERGY_FULL: + val->intval = ab8500_fg_convert_mah_to_uwh(di, + di->bat_cap.max_mah); + break; + case POWER_SUPPLY_PROP_ENERGY_NOW: + if (di->flags.batt_unknown && !di->bm->chg_unknown_bat && + di->flags.batt_id_received) + val->intval = ab8500_fg_convert_mah_to_uwh(di, + di->bat_cap.max_mah); + else + val->intval = ab8500_fg_convert_mah_to_uwh(di, + di->bat_cap.prev_mah); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = di->bat_cap.max_mah_design; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = di->bat_cap.max_mah; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + if (di->flags.batt_unknown && !di->bm->chg_unknown_bat && + di->flags.batt_id_received) + val->intval = di->bat_cap.max_mah; + else + val->intval = di->bat_cap.prev_mah; + break; + case POWER_SUPPLY_PROP_CAPACITY: + if (di->flags.batt_unknown && !di->bm->chg_unknown_bat && + di->flags.batt_id_received) + val->intval = 100; + else + val->intval = di->bat_cap.prev_percent; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + if (di->flags.batt_unknown && !di->bm->chg_unknown_bat && + di->flags.batt_id_received) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + else + val->intval = di->bat_cap.prev_level; + break; + default: + return -EINVAL; + } + return 0; +} + +static int ab8500_fg_get_ext_psy_data(struct device *dev, void *data) +{ + struct power_supply *psy; + struct power_supply *ext = dev_get_drvdata(dev); + const char **supplicants = (const char **)ext->supplied_to; + struct ab8500_fg *di; + union power_supply_propval ret; + int j; + + psy = (struct power_supply *)data; + di = power_supply_get_drvdata(psy); + + /* + * For all psy where the name of your driver + * appears in any supplied_to + */ + j = match_string(supplicants, ext->num_supplicants, psy->desc->name); + if (j < 0) + return 0; + + /* Go through all properties for the psy */ + for (j = 0; j < ext->desc->num_properties; j++) { + enum power_supply_property prop; + prop = ext->desc->properties[j]; + + if (power_supply_get_property(ext, prop, &ret)) + continue; + + switch (prop) { + case POWER_SUPPLY_PROP_STATUS: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + switch (ret.intval) { + case POWER_SUPPLY_STATUS_UNKNOWN: + case POWER_SUPPLY_STATUS_DISCHARGING: + case POWER_SUPPLY_STATUS_NOT_CHARGING: + if (!di->flags.charging) + break; + di->flags.charging = false; + di->flags.fully_charged = false; + if (di->bm->capacity_scaling) + ab8500_fg_update_cap_scalers(di); + queue_work(di->fg_wq, &di->fg_work); + break; + case POWER_SUPPLY_STATUS_FULL: + if (di->flags.fully_charged) + break; + di->flags.fully_charged = true; + di->flags.force_full = true; + /* Save current capacity as maximum */ + di->bat_cap.max_mah = di->bat_cap.mah; + queue_work(di->fg_wq, &di->fg_work); + break; + case POWER_SUPPLY_STATUS_CHARGING: + if (di->flags.charging && + !di->flags.fully_charged) + break; + di->flags.charging = true; + di->flags.fully_charged = false; + if (di->bm->capacity_scaling) + ab8500_fg_update_cap_scalers(di); + queue_work(di->fg_wq, &di->fg_work); + break; + } + default: + break; + } + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + if (!di->flags.batt_id_received && + di->bm->batt_id != BATTERY_UNKNOWN) { + const struct abx500_battery_type *b; + + b = &(di->bm->bat_type[di->bm->batt_id]); + + di->flags.batt_id_received = true; + + di->bat_cap.max_mah_design = + MILLI_TO_MICRO * + b->charge_full_design; + + di->bat_cap.max_mah = + di->bat_cap.max_mah_design; + + di->vbat_nom = b->nominal_voltage; + } + + if (ret.intval) + di->flags.batt_unknown = false; + else + di->flags.batt_unknown = true; + break; + default: + break; + } + break; + case POWER_SUPPLY_PROP_TEMP: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + if (di->flags.batt_id_received) + di->bat_temp = ret.intval; + break; + default: + break; + } + break; + default: + break; + } + } + return 0; +} + +/** + * ab8500_fg_init_hw_registers() - Set up FG related registers + * @di: pointer to the ab8500_fg structure + * + * Set up battery OVV, low battery voltage registers + */ +static int ab8500_fg_init_hw_registers(struct ab8500_fg *di) +{ + int ret; + + /* Set VBAT OVV threshold */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_BATT_OVV, + BATT_OVV_TH_4P75, + BATT_OVV_TH_4P75); + if (ret) { + dev_err(di->dev, "failed to set BATT_OVV\n"); + goto out; + } + + /* Enable VBAT OVV detection */ + ret = abx500_mask_and_set_register_interruptible(di->dev, + AB8500_CHARGER, + AB8500_BATT_OVV, + BATT_OVV_ENA, + BATT_OVV_ENA); + if (ret) { + dev_err(di->dev, "failed to enable BATT_OVV\n"); + goto out; + } + + /* Low Battery Voltage */ + ret = abx500_set_register_interruptible(di->dev, + AB8500_SYS_CTRL2_BLOCK, + AB8500_LOW_BAT_REG, + ab8500_volt_to_regval( + di->bm->fg_params->lowbat_threshold) << 1 | + LOW_BAT_ENABLE); + if (ret) { + dev_err(di->dev, "%s write failed\n", __func__); + goto out; + } + + /* Battery OK threshold */ + ret = ab8500_fg_battok_init_hw_register(di); + if (ret) { + dev_err(di->dev, "BattOk init write failed.\n"); + goto out; + } + + if (is_ab8505(di->parent)) { + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_MAX_TIME_REG, di->bm->fg_params->pcut_max_time); + + if (ret) { + dev_err(di->dev, "%s write failed AB8505_RTC_PCUT_MAX_TIME_REG\n", __func__); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_FLAG_TIME_REG, di->bm->fg_params->pcut_flag_time); + + if (ret) { + dev_err(di->dev, "%s write failed AB8505_RTC_PCUT_FLAG_TIME_REG\n", __func__); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_RESTART_REG, di->bm->fg_params->pcut_max_restart); + + if (ret) { + dev_err(di->dev, "%s write failed AB8505_RTC_PCUT_RESTART_REG\n", __func__); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_DEBOUNCE_REG, di->bm->fg_params->pcut_debounce_time); + + if (ret) { + dev_err(di->dev, "%s write failed AB8505_RTC_PCUT_DEBOUNCE_REG\n", __func__); + goto out; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_CTL_STATUS_REG, di->bm->fg_params->pcut_enable); + + if (ret) { + dev_err(di->dev, "%s write failed AB8505_RTC_PCUT_CTL_STATUS_REG\n", __func__); + goto out; + } + } +out: + return ret; +} + +/** + * ab8500_fg_external_power_changed() - callback for power supply changes + * @psy: pointer to the structure power_supply + * + * This function is the entry point of the pointer external_power_changed + * of the structure power_supply. + * This function gets executed when there is a change in any external power + * supply that this driver needs to be notified of. + */ +static void ab8500_fg_external_power_changed(struct power_supply *psy) +{ + class_for_each_device(power_supply_class, NULL, psy, + ab8500_fg_get_ext_psy_data); +} + +/** + * ab8500_fg_reinit_work() - work to reset the FG algorithm + * @work: pointer to the work_struct structure + * + * Used to reset the current battery capacity to be able to + * retrigger a new voltage base capacity calculation. For + * test and verification purpose. + */ +static void ab8500_fg_reinit_work(struct work_struct *work) +{ + struct ab8500_fg *di = container_of(work, struct ab8500_fg, + fg_reinit_work.work); + + if (!di->flags.calibrate) { + dev_dbg(di->dev, "Resetting FG state machine to init.\n"); + ab8500_fg_clear_cap_samples(di); + ab8500_fg_calc_cap_discharge_voltage(di, true); + ab8500_fg_charge_state_to(di, AB8500_FG_CHARGE_INIT); + ab8500_fg_discharge_state_to(di, AB8500_FG_DISCHARGE_INIT); + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + + } else { + dev_err(di->dev, "Residual offset calibration ongoing " + "retrying..\n"); + /* Wait one second until next try*/ + queue_delayed_work(di->fg_wq, &di->fg_reinit_work, + round_jiffies(1)); + } +} + +/* Exposure to the sysfs interface */ + +struct ab8500_fg_sysfs_entry { + struct attribute attr; + ssize_t (*show)(struct ab8500_fg *, char *); + ssize_t (*store)(struct ab8500_fg *, const char *, size_t); +}; + +static ssize_t charge_full_show(struct ab8500_fg *di, char *buf) +{ + return sprintf(buf, "%d\n", di->bat_cap.max_mah); +} + +static ssize_t charge_full_store(struct ab8500_fg *di, const char *buf, + size_t count) +{ + unsigned long charge_full; + int ret; + + ret = kstrtoul(buf, 10, &charge_full); + if (ret) + return ret; + + di->bat_cap.max_mah = (int) charge_full; + return count; +} + +static ssize_t charge_now_show(struct ab8500_fg *di, char *buf) +{ + return sprintf(buf, "%d\n", di->bat_cap.prev_mah); +} + +static ssize_t charge_now_store(struct ab8500_fg *di, const char *buf, + size_t count) +{ + unsigned long charge_now; + int ret; + + ret = kstrtoul(buf, 10, &charge_now); + if (ret) + return ret; + + di->bat_cap.user_mah = (int) charge_now; + di->flags.user_cap = true; + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + return count; +} + +static struct ab8500_fg_sysfs_entry charge_full_attr = + __ATTR(charge_full, 0644, charge_full_show, charge_full_store); + +static struct ab8500_fg_sysfs_entry charge_now_attr = + __ATTR(charge_now, 0644, charge_now_show, charge_now_store); + +static ssize_t +ab8500_fg_show(struct kobject *kobj, struct attribute *attr, char *buf) +{ + struct ab8500_fg_sysfs_entry *entry; + struct ab8500_fg *di; + + entry = container_of(attr, struct ab8500_fg_sysfs_entry, attr); + di = container_of(kobj, struct ab8500_fg, fg_kobject); + + if (!entry->show) + return -EIO; + + return entry->show(di, buf); +} +static ssize_t +ab8500_fg_store(struct kobject *kobj, struct attribute *attr, const char *buf, + size_t count) +{ + struct ab8500_fg_sysfs_entry *entry; + struct ab8500_fg *di; + + entry = container_of(attr, struct ab8500_fg_sysfs_entry, attr); + di = container_of(kobj, struct ab8500_fg, fg_kobject); + + if (!entry->store) + return -EIO; + + return entry->store(di, buf, count); +} + +static const struct sysfs_ops ab8500_fg_sysfs_ops = { + .show = ab8500_fg_show, + .store = ab8500_fg_store, +}; + +static struct attribute *ab8500_fg_attrs[] = { + &charge_full_attr.attr, + &charge_now_attr.attr, + NULL, +}; + +static struct kobj_type ab8500_fg_ktype = { + .sysfs_ops = &ab8500_fg_sysfs_ops, + .default_attrs = ab8500_fg_attrs, +}; + +/** + * ab8500_fg_sysfs_exit() - de-init of sysfs entry + * @di: pointer to the struct ab8500_chargalg + * + * This function removes the entry in sysfs. + */ +static void ab8500_fg_sysfs_exit(struct ab8500_fg *di) +{ + kobject_del(&di->fg_kobject); +} + +/** + * ab8500_fg_sysfs_init() - init of sysfs entry + * @di: pointer to the struct ab8500_chargalg + * + * This function adds an entry in sysfs. + * Returns error code in case of failure else 0(on success) + */ +static int ab8500_fg_sysfs_init(struct ab8500_fg *di) +{ + int ret = 0; + + ret = kobject_init_and_add(&di->fg_kobject, + &ab8500_fg_ktype, + NULL, "battery"); + if (ret < 0) { + kobject_put(&di->fg_kobject); + dev_err(di->dev, "failed to create sysfs entry\n"); + } + + return ret; +} + +static ssize_t ab8505_powercut_flagtime_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_FLAG_TIME_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_FLAG_TIME_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0x7F)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_flagtime_write(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + if (kstrtoint(buf, 10, ®_value)) + goto fail; + + if (reg_value > 0x7F) { + dev_err(dev, "Incorrect parameter, echo 0 (1.98s) - 127 (15.625ms) for flagtime\n"); + goto fail; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_FLAG_TIME_REG, (u8)reg_value); + + if (ret < 0) + dev_err(dev, "Failed to set AB8505_RTC_PCUT_FLAG_TIME_REG\n"); + +fail: + return count; +} + +static ssize_t ab8505_powercut_maxtime_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_MAX_TIME_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_MAX_TIME_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0x7F)); + +fail: + return ret; + +} + +static ssize_t ab8505_powercut_maxtime_write(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + if (kstrtoint(buf, 10, ®_value)) + goto fail; + + if (reg_value > 0x7F) { + dev_err(dev, "Incorrect parameter, echo 0 (0.0s) - 127 (1.98s) for maxtime\n"); + goto fail; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_MAX_TIME_REG, (u8)reg_value); + + if (ret < 0) + dev_err(dev, "Failed to set AB8505_RTC_PCUT_MAX_TIME_REG\n"); + +fail: + return count; +} + +static ssize_t ab8505_powercut_restart_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_RESTART_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_RESTART_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0xF)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_restart_write(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + if (kstrtoint(buf, 10, ®_value)) + goto fail; + + if (reg_value > 0xF) { + dev_err(dev, "Incorrect parameter, echo 0 - 15 for number of restart\n"); + goto fail; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_RESTART_REG, (u8)reg_value); + + if (ret < 0) + dev_err(dev, "Failed to set AB8505_RTC_PCUT_RESTART_REG\n"); + +fail: + return count; + +} + +static ssize_t ab8505_powercut_timer_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_TIME_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_TIME_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0x7F)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_restart_counter_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_RESTART_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_RESTART_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0xF0) >> 4); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_CTL_STATUS_REG, ®_value); + + if (ret < 0) + goto fail; + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0x1)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_write(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + if (kstrtoint(buf, 10, ®_value)) + goto fail; + + if (reg_value > 0x1) { + dev_err(dev, "Incorrect parameter, echo 0/1 to disable/enable Pcut feature\n"); + goto fail; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_CTL_STATUS_REG, (u8)reg_value); + + if (ret < 0) + dev_err(dev, "Failed to set AB8505_RTC_PCUT_CTL_STATUS_REG\n"); + +fail: + return count; +} + +static ssize_t ab8505_powercut_flag_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_CTL_STATUS_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_CTL_STATUS_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", ((reg_value & 0x10) >> 4)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_debounce_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_DEBOUNCE_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_DEBOUNCE_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", (reg_value & 0x7)); + +fail: + return ret; +} + +static ssize_t ab8505_powercut_debounce_write(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + int reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + if (kstrtoint(buf, 10, ®_value)) + goto fail; + + if (reg_value > 0x7) { + dev_err(dev, "Incorrect parameter, echo 0 to 7 for debounce setting\n"); + goto fail; + } + + ret = abx500_set_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_DEBOUNCE_REG, (u8)reg_value); + + if (ret < 0) + dev_err(dev, "Failed to set AB8505_RTC_PCUT_DEBOUNCE_REG\n"); + +fail: + return count; +} + +static ssize_t ab8505_powercut_enable_status_read(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 reg_value; + struct power_supply *psy = dev_get_drvdata(dev); + struct ab8500_fg *di = power_supply_get_drvdata(psy); + + ret = abx500_get_register_interruptible(di->dev, AB8500_RTC, + AB8505_RTC_PCUT_CTL_STATUS_REG, ®_value); + + if (ret < 0) { + dev_err(dev, "Failed to read AB8505_RTC_PCUT_CTL_STATUS_REG\n"); + goto fail; + } + + return scnprintf(buf, PAGE_SIZE, "%d\n", ((reg_value & 0x20) >> 5)); + +fail: + return ret; +} + +static struct device_attribute ab8505_fg_sysfs_psy_attrs[] = { + __ATTR(powercut_flagtime, (S_IRUGO | S_IWUSR | S_IWGRP), + ab8505_powercut_flagtime_read, ab8505_powercut_flagtime_write), + __ATTR(powercut_maxtime, (S_IRUGO | S_IWUSR | S_IWGRP), + ab8505_powercut_maxtime_read, ab8505_powercut_maxtime_write), + __ATTR(powercut_restart_max, (S_IRUGO | S_IWUSR | S_IWGRP), + ab8505_powercut_restart_read, ab8505_powercut_restart_write), + __ATTR(powercut_timer, S_IRUGO, ab8505_powercut_timer_read, NULL), + __ATTR(powercut_restart_counter, S_IRUGO, + ab8505_powercut_restart_counter_read, NULL), + __ATTR(powercut_enable, (S_IRUGO | S_IWUSR | S_IWGRP), + ab8505_powercut_read, ab8505_powercut_write), + __ATTR(powercut_flag, S_IRUGO, ab8505_powercut_flag_read, NULL), + __ATTR(powercut_debounce_time, (S_IRUGO | S_IWUSR | S_IWGRP), + ab8505_powercut_debounce_read, ab8505_powercut_debounce_write), + __ATTR(powercut_enable_status, S_IRUGO, + ab8505_powercut_enable_status_read, NULL), +}; + +static int ab8500_fg_sysfs_psy_create_attrs(struct ab8500_fg *di) +{ + unsigned int i; + + if (is_ab8505(di->parent)) { + for (i = 0; i < ARRAY_SIZE(ab8505_fg_sysfs_psy_attrs); i++) + if (device_create_file(&di->fg_psy->dev, + &ab8505_fg_sysfs_psy_attrs[i])) + goto sysfs_psy_create_attrs_failed_ab8505; + } + return 0; +sysfs_psy_create_attrs_failed_ab8505: + dev_err(&di->fg_psy->dev, "Failed creating sysfs psy attrs for ab8505.\n"); + while (i--) + device_remove_file(&di->fg_psy->dev, + &ab8505_fg_sysfs_psy_attrs[i]); + + return -EIO; +} + +static void ab8500_fg_sysfs_psy_remove_attrs(struct ab8500_fg *di) +{ + unsigned int i; + + if (is_ab8505(di->parent)) { + for (i = 0; i < ARRAY_SIZE(ab8505_fg_sysfs_psy_attrs); i++) + (void)device_remove_file(&di->fg_psy->dev, + &ab8505_fg_sysfs_psy_attrs[i]); + } +} + +/* Exposure to the sysfs interface <<END>> */ + +#if defined(CONFIG_PM) +static int ab8500_fg_resume(struct platform_device *pdev) +{ + struct ab8500_fg *di = platform_get_drvdata(pdev); + + /* + * Change state if we're not charging. If we're charging we will wake + * up on the FG IRQ + */ + if (!di->flags.charging) { + ab8500_fg_discharge_state_to(di, AB8500_FG_DISCHARGE_WAKEUP); + queue_work(di->fg_wq, &di->fg_work); + } + + return 0; +} + +static int ab8500_fg_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct ab8500_fg *di = platform_get_drvdata(pdev); + + flush_delayed_work(&di->fg_periodic_work); + flush_work(&di->fg_work); + flush_work(&di->fg_acc_cur_work); + flush_delayed_work(&di->fg_reinit_work); + flush_delayed_work(&di->fg_low_bat_work); + flush_delayed_work(&di->fg_check_hw_failure_work); + + /* + * If the FG is enabled we will disable it before going to suspend + * only if we're not charging + */ + if (di->flags.fg_enabled && !di->flags.charging) + ab8500_fg_coulomb_counter(di, false); + + return 0; +} +#else +#define ab8500_fg_suspend NULL +#define ab8500_fg_resume NULL +#endif + +static int ab8500_fg_remove(struct platform_device *pdev) +{ + int ret = 0; + struct ab8500_fg *di = platform_get_drvdata(pdev); + + list_del(&di->node); + + /* Disable coulomb counter */ + ret = ab8500_fg_coulomb_counter(di, false); + if (ret) + dev_err(di->dev, "failed to disable coulomb counter\n"); + + destroy_workqueue(di->fg_wq); + ab8500_fg_sysfs_exit(di); + + flush_scheduled_work(); + ab8500_fg_sysfs_psy_remove_attrs(di); + power_supply_unregister(di->fg_psy); + return ret; +} + +/* ab8500 fg driver interrupts and their respective isr */ +static struct ab8500_fg_interrupts ab8500_fg_irq_th[] = { + {"NCONV_ACCU", ab8500_fg_cc_convend_handler}, + {"BATT_OVV", ab8500_fg_batt_ovv_handler}, + {"LOW_BAT_F", ab8500_fg_lowbatf_handler}, + {"CC_INT_CALIB", ab8500_fg_cc_int_calib_handler}, +}; + +static struct ab8500_fg_interrupts ab8500_fg_irq_bh[] = { + {"CCEOC", ab8500_fg_cc_data_end_handler}, +}; + +static char *supply_interface[] = { + "ab8500_chargalg", + "ab8500_usb", +}; + +static const struct power_supply_desc ab8500_fg_desc = { + .name = "ab8500_fg", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = ab8500_fg_props, + .num_properties = ARRAY_SIZE(ab8500_fg_props), + .get_property = ab8500_fg_get_property, + .external_power_changed = ab8500_fg_external_power_changed, +}; + +static int ab8500_fg_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct abx500_bm_data *plat = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + struct ab8500_fg *di; + int i, irq; + int ret = 0; + + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); + if (!di) { + dev_err(&pdev->dev, "%s no mem for ab8500_fg\n", __func__); + return -ENOMEM; + } + + if (!plat) { + dev_err(&pdev->dev, "no battery management data supplied\n"); + return -EINVAL; + } + di->bm = plat; + + if (np) { + ret = ab8500_bm_of_probe(&pdev->dev, np, di->bm); + if (ret) { + dev_err(&pdev->dev, "failed to get battery information\n"); + return ret; + } + } + + mutex_init(&di->cc_lock); + + /* get parent data */ + di->dev = &pdev->dev; + di->parent = dev_get_drvdata(pdev->dev.parent); + + di->main_bat_v = devm_iio_channel_get(&pdev->dev, "main_bat_v"); + if (IS_ERR(di->main_bat_v)) { + if (PTR_ERR(di->main_bat_v) == -ENODEV) + return -EPROBE_DEFER; + dev_err(&pdev->dev, "failed to get main battery ADC channel\n"); + return PTR_ERR(di->main_bat_v); + } + + psy_cfg.supplied_to = supply_interface; + psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); + psy_cfg.drv_data = di; + + di->bat_cap.max_mah_design = MILLI_TO_MICRO * + di->bm->bat_type[di->bm->batt_id].charge_full_design; + + di->bat_cap.max_mah = di->bat_cap.max_mah_design; + + di->vbat_nom = di->bm->bat_type[di->bm->batt_id].nominal_voltage; + + di->init_capacity = true; + + ab8500_fg_charge_state_to(di, AB8500_FG_CHARGE_INIT); + ab8500_fg_discharge_state_to(di, AB8500_FG_DISCHARGE_INIT); + + /* Create a work queue for running the FG algorithm */ + di->fg_wq = alloc_ordered_workqueue("ab8500_fg_wq", WQ_MEM_RECLAIM); + if (di->fg_wq == NULL) { + dev_err(di->dev, "failed to create work queue\n"); + return -ENOMEM; + } + + /* Init work for running the fg algorithm instantly */ + INIT_WORK(&di->fg_work, ab8500_fg_instant_work); + + /* Init work for getting the battery accumulated current */ + INIT_WORK(&di->fg_acc_cur_work, ab8500_fg_acc_cur_work); + + /* Init work for reinitialising the fg algorithm */ + INIT_DEFERRABLE_WORK(&di->fg_reinit_work, + ab8500_fg_reinit_work); + + /* Work delayed Queue to run the state machine */ + INIT_DEFERRABLE_WORK(&di->fg_periodic_work, + ab8500_fg_periodic_work); + + /* Work to check low battery condition */ + INIT_DEFERRABLE_WORK(&di->fg_low_bat_work, + ab8500_fg_low_bat_work); + + /* Init work for HW failure check */ + INIT_DEFERRABLE_WORK(&di->fg_check_hw_failure_work, + ab8500_fg_check_hw_failure_work); + + /* Reset battery low voltage flag */ + di->flags.low_bat = false; + + /* Initialize low battery counter */ + di->low_bat_cnt = 10; + + /* Initialize OVV, and other registers */ + ret = ab8500_fg_init_hw_registers(di); + if (ret) { + dev_err(di->dev, "failed to initialize registers\n"); + goto free_inst_curr_wq; + } + + /* Consider battery unknown until we're informed otherwise */ + di->flags.batt_unknown = true; + di->flags.batt_id_received = false; + + /* Register FG power supply class */ + di->fg_psy = power_supply_register(di->dev, &ab8500_fg_desc, &psy_cfg); + if (IS_ERR(di->fg_psy)) { + dev_err(di->dev, "failed to register FG psy\n"); + ret = PTR_ERR(di->fg_psy); + goto free_inst_curr_wq; + } + + di->fg_samples = SEC_TO_SAMPLE(di->bm->fg_params->init_timer); + ab8500_fg_coulomb_counter(di, true); + + /* + * Initialize completion used to notify completion and start + * of inst current + */ + init_completion(&di->ab8500_fg_started); + init_completion(&di->ab8500_fg_complete); + + /* Register primary interrupt handlers */ + for (i = 0; i < ARRAY_SIZE(ab8500_fg_irq_th); i++) { + irq = platform_get_irq_byname(pdev, ab8500_fg_irq_th[i].name); + if (irq < 0) { + ret = irq; + goto free_irq_th; + } + + ret = request_irq(irq, ab8500_fg_irq_th[i].isr, + IRQF_SHARED | IRQF_NO_SUSPEND, + ab8500_fg_irq_th[i].name, di); + + if (ret != 0) { + dev_err(di->dev, "failed to request %s IRQ %d: %d\n", + ab8500_fg_irq_th[i].name, irq, ret); + goto free_irq_th; + } + dev_dbg(di->dev, "Requested %s IRQ %d: %d\n", + ab8500_fg_irq_th[i].name, irq, ret); + } + + /* Register threaded interrupt handler */ + irq = platform_get_irq_byname(pdev, ab8500_fg_irq_bh[0].name); + if (irq < 0) { + ret = irq; + goto free_irq_th; + } + + ret = request_threaded_irq(irq, NULL, ab8500_fg_irq_bh[0].isr, + IRQF_SHARED | IRQF_NO_SUSPEND | IRQF_ONESHOT, + ab8500_fg_irq_bh[0].name, di); + + if (ret != 0) { + dev_err(di->dev, "failed to request %s IRQ %d: %d\n", + ab8500_fg_irq_bh[0].name, irq, ret); + goto free_irq_th; + } + dev_dbg(di->dev, "Requested %s IRQ %d: %d\n", + ab8500_fg_irq_bh[0].name, irq, ret); + + di->irq = platform_get_irq_byname(pdev, "CCEOC"); + disable_irq(di->irq); + di->nbr_cceoc_irq_cnt = 0; + + platform_set_drvdata(pdev, di); + + ret = ab8500_fg_sysfs_init(di); + if (ret) { + dev_err(di->dev, "failed to create sysfs entry\n"); + goto free_irq; + } + + ret = ab8500_fg_sysfs_psy_create_attrs(di); + if (ret) { + dev_err(di->dev, "failed to create FG psy\n"); + ab8500_fg_sysfs_exit(di); + goto free_irq; + } + + /* Calibrate the fg first time */ + di->flags.calibrate = true; + di->calib_state = AB8500_FG_CALIB_INIT; + + /* Use room temp as default value until we get an update from driver. */ + di->bat_temp = 210; + + /* Run the FG algorithm */ + queue_delayed_work(di->fg_wq, &di->fg_periodic_work, 0); + + list_add_tail(&di->node, &ab8500_fg_list); + + return ret; + +free_irq: + /* We also have to free all registered irqs */ + irq = platform_get_irq_byname(pdev, ab8500_fg_irq_bh[0].name); + free_irq(irq, di); +free_irq_th: + while (--i >= 0) { + /* Last assignment of i from primary interrupt handlers */ + irq = platform_get_irq_byname(pdev, ab8500_fg_irq_th[i].name); + free_irq(irq, di); + } + + power_supply_unregister(di->fg_psy); +free_inst_curr_wq: + destroy_workqueue(di->fg_wq); + return ret; +} + +static const struct of_device_id ab8500_fg_match[] = { + { .compatible = "stericsson,ab8500-fg", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ab8500_fg_match); + +static struct platform_driver ab8500_fg_driver = { + .probe = ab8500_fg_probe, + .remove = ab8500_fg_remove, + .suspend = ab8500_fg_suspend, + .resume = ab8500_fg_resume, + .driver = { + .name = "ab8500-fg", + .of_match_table = ab8500_fg_match, + }, +}; + +static int __init ab8500_fg_init(void) +{ + return platform_driver_register(&ab8500_fg_driver); +} + +static void __exit ab8500_fg_exit(void) +{ + platform_driver_unregister(&ab8500_fg_driver); +} + +subsys_initcall_sync(ab8500_fg_init); +module_exit(ab8500_fg_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Johan Palsson, Karl Komierowski"); +MODULE_ALIAS("platform:ab8500-fg"); +MODULE_DESCRIPTION("AB8500 Fuel Gauge driver"); diff --git a/drivers/power/supply/abx500_chargalg.c b/drivers/power/supply/abx500_chargalg.c new file mode 100644 index 000000000..175c4f3d7 --- /dev/null +++ b/drivers/power/supply/abx500_chargalg.c @@ -0,0 +1,2104 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) ST-Ericsson SA 2012 + * Copyright (c) 2012 Sony Mobile Communications AB + * + * Charging algorithm driver for abx500 variants + * + * Authors: + * Johan Palsson <johan.palsson@stericsson.com> + * Karl Komierowski <karl.komierowski@stericsson.com> + * Arun R Murthy <arun.murthy@stericsson.com> + * Author: Imre Sunyi <imre.sunyi@sonymobile.com> + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/hrtimer.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/completion.h> +#include <linux/workqueue.h> +#include <linux/kobject.h> +#include <linux/of.h> +#include <linux/mfd/core.h> +#include <linux/mfd/abx500.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500/ux500_chargalg.h> +#include <linux/mfd/abx500/ab8500-bm.h> +#include <linux/notifier.h> + +/* Watchdog kick interval */ +#define CHG_WD_INTERVAL (6 * HZ) + +/* End-of-charge criteria counter */ +#define EOC_COND_CNT 10 + +/* One hour expressed in seconds */ +#define ONE_HOUR_IN_SECONDS 3600 + +/* Five minutes expressed in seconds */ +#define FIVE_MINUTES_IN_SECONDS 300 + +#define CHARGALG_CURR_STEP_LOW 0 +#define CHARGALG_CURR_STEP_HIGH 100 + +enum abx500_chargers { + NO_CHG, + AC_CHG, + USB_CHG, +}; + +struct abx500_chargalg_charger_info { + enum abx500_chargers conn_chg; + enum abx500_chargers prev_conn_chg; + enum abx500_chargers online_chg; + enum abx500_chargers prev_online_chg; + enum abx500_chargers charger_type; + bool usb_chg_ok; + bool ac_chg_ok; + int usb_volt; + int usb_curr; + int ac_volt; + int ac_curr; + int usb_vset; + int usb_iset; + int ac_vset; + int ac_iset; +}; + +struct abx500_chargalg_suspension_status { + bool suspended_change; + bool ac_suspended; + bool usb_suspended; +}; + +struct abx500_chargalg_current_step_status { + bool curr_step_change; + int curr_step; +}; + +struct abx500_chargalg_battery_data { + int temp; + int volt; + int avg_curr; + int inst_curr; + int percent; +}; + +enum abx500_chargalg_states { + STATE_HANDHELD_INIT, + STATE_HANDHELD, + STATE_CHG_NOT_OK_INIT, + STATE_CHG_NOT_OK, + STATE_HW_TEMP_PROTECT_INIT, + STATE_HW_TEMP_PROTECT, + STATE_NORMAL_INIT, + STATE_NORMAL, + STATE_WAIT_FOR_RECHARGE_INIT, + STATE_WAIT_FOR_RECHARGE, + STATE_MAINTENANCE_A_INIT, + STATE_MAINTENANCE_A, + STATE_MAINTENANCE_B_INIT, + STATE_MAINTENANCE_B, + STATE_TEMP_UNDEROVER_INIT, + STATE_TEMP_UNDEROVER, + STATE_TEMP_LOWHIGH_INIT, + STATE_TEMP_LOWHIGH, + STATE_SUSPENDED_INIT, + STATE_SUSPENDED, + STATE_OVV_PROTECT_INIT, + STATE_OVV_PROTECT, + STATE_SAFETY_TIMER_EXPIRED_INIT, + STATE_SAFETY_TIMER_EXPIRED, + STATE_BATT_REMOVED_INIT, + STATE_BATT_REMOVED, + STATE_WD_EXPIRED_INIT, + STATE_WD_EXPIRED, +}; + +static const char *states[] = { + "HANDHELD_INIT", + "HANDHELD", + "CHG_NOT_OK_INIT", + "CHG_NOT_OK", + "HW_TEMP_PROTECT_INIT", + "HW_TEMP_PROTECT", + "NORMAL_INIT", + "NORMAL", + "WAIT_FOR_RECHARGE_INIT", + "WAIT_FOR_RECHARGE", + "MAINTENANCE_A_INIT", + "MAINTENANCE_A", + "MAINTENANCE_B_INIT", + "MAINTENANCE_B", + "TEMP_UNDEROVER_INIT", + "TEMP_UNDEROVER", + "TEMP_LOWHIGH_INIT", + "TEMP_LOWHIGH", + "SUSPENDED_INIT", + "SUSPENDED", + "OVV_PROTECT_INIT", + "OVV_PROTECT", + "SAFETY_TIMER_EXPIRED_INIT", + "SAFETY_TIMER_EXPIRED", + "BATT_REMOVED_INIT", + "BATT_REMOVED", + "WD_EXPIRED_INIT", + "WD_EXPIRED", +}; + +struct abx500_chargalg_events { + bool batt_unknown; + bool mainextchnotok; + bool batt_ovv; + bool batt_rem; + bool btemp_underover; + bool btemp_lowhigh; + bool main_thermal_prot; + bool usb_thermal_prot; + bool main_ovv; + bool vbus_ovv; + bool usbchargernotok; + bool safety_timer_expired; + bool maintenance_timer_expired; + bool ac_wd_expired; + bool usb_wd_expired; + bool ac_cv_active; + bool usb_cv_active; + bool vbus_collapsed; +}; + +/** + * struct abx500_charge_curr_maximization - Charger maximization parameters + * @original_iset: the non optimized/maximised charger current + * @current_iset: the charging current used at this moment + * @test_delta_i: the delta between the current we want to charge and the + current that is really going into the battery + * @condition_cnt: number of iterations needed before a new charger current + is set + * @max_current: maximum charger current + * @wait_cnt: to avoid too fast current step down in case of charger + * voltage collapse, we insert this delay between step + * down + * @level: tells in how many steps the charging current has been + increased + */ +struct abx500_charge_curr_maximization { + int original_iset; + int current_iset; + int test_delta_i; + int condition_cnt; + int max_current; + int wait_cnt; + u8 level; +}; + +enum maxim_ret { + MAXIM_RET_NOACTION, + MAXIM_RET_CHANGE, + MAXIM_RET_IBAT_TOO_HIGH, +}; + +/** + * struct abx500_chargalg - abx500 Charging algorithm device information + * @dev: pointer to the structure device + * @charge_status: battery operating status + * @eoc_cnt: counter used to determine end-of_charge + * @maintenance_chg: indicate if maintenance charge is active + * @t_hyst_norm temperature hysteresis when the temperature has been + * over or under normal limits + * @t_hyst_lowhigh temperature hysteresis when the temperature has been + * over or under the high or low limits + * @charge_state: current state of the charging algorithm + * @ccm charging current maximization parameters + * @chg_info: information about connected charger types + * @batt_data: data of the battery + * @susp_status: current charger suspension status + * @bm: Platform specific battery management information + * @curr_status: Current step status for over-current protection + * @parent: pointer to the struct abx500 + * @chargalg_psy: structure that holds the battery properties exposed by + * the charging algorithm + * @events: structure for information about events triggered + * @chargalg_wq: work queue for running the charging algorithm + * @chargalg_periodic_work: work to run the charging algorithm periodically + * @chargalg_wd_work: work to kick the charger watchdog periodically + * @chargalg_work: work to run the charging algorithm instantly + * @safety_timer: charging safety timer + * @maintenance_timer: maintenance charging timer + * @chargalg_kobject: structure of type kobject + */ +struct abx500_chargalg { + struct device *dev; + int charge_status; + int eoc_cnt; + bool maintenance_chg; + int t_hyst_norm; + int t_hyst_lowhigh; + enum abx500_chargalg_states charge_state; + struct abx500_charge_curr_maximization ccm; + struct abx500_chargalg_charger_info chg_info; + struct abx500_chargalg_battery_data batt_data; + struct abx500_chargalg_suspension_status susp_status; + struct ab8500 *parent; + struct abx500_chargalg_current_step_status curr_status; + struct abx500_bm_data *bm; + struct power_supply *chargalg_psy; + struct ux500_charger *ac_chg; + struct ux500_charger *usb_chg; + struct abx500_chargalg_events events; + struct workqueue_struct *chargalg_wq; + struct delayed_work chargalg_periodic_work; + struct delayed_work chargalg_wd_work; + struct work_struct chargalg_work; + struct hrtimer safety_timer; + struct hrtimer maintenance_timer; + struct kobject chargalg_kobject; +}; + +/*External charger prepare notifier*/ +BLOCKING_NOTIFIER_HEAD(charger_notifier_list); + +/* Main battery properties */ +static enum power_supply_property abx500_chargalg_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, +}; + +struct abx500_chargalg_sysfs_entry { + struct attribute attr; + ssize_t (*show)(struct abx500_chargalg *, char *); + ssize_t (*store)(struct abx500_chargalg *, const char *, size_t); +}; + +/** + * abx500_chargalg_safety_timer_expired() - Expiration of the safety timer + * @timer: pointer to the hrtimer structure + * + * This function gets called when the safety timer for the charger + * expires + */ +static enum hrtimer_restart +abx500_chargalg_safety_timer_expired(struct hrtimer *timer) +{ + struct abx500_chargalg *di = container_of(timer, struct abx500_chargalg, + safety_timer); + dev_err(di->dev, "Safety timer expired\n"); + di->events.safety_timer_expired = true; + + /* Trigger execution of the algorithm instantly */ + queue_work(di->chargalg_wq, &di->chargalg_work); + + return HRTIMER_NORESTART; +} + +/** + * abx500_chargalg_maintenance_timer_expired() - Expiration of + * the maintenance timer + * @timer: pointer to the timer structure + * + * This function gets called when the maintenence timer + * expires + */ +static enum hrtimer_restart +abx500_chargalg_maintenance_timer_expired(struct hrtimer *timer) +{ + + struct abx500_chargalg *di = container_of(timer, struct abx500_chargalg, + maintenance_timer); + + dev_dbg(di->dev, "Maintenance timer expired\n"); + di->events.maintenance_timer_expired = true; + + /* Trigger execution of the algorithm instantly */ + queue_work(di->chargalg_wq, &di->chargalg_work); + + return HRTIMER_NORESTART; +} + +/** + * abx500_chargalg_state_to() - Change charge state + * @di: pointer to the abx500_chargalg structure + * + * This function gets called when a charge state change should occur + */ +static void abx500_chargalg_state_to(struct abx500_chargalg *di, + enum abx500_chargalg_states state) +{ + dev_dbg(di->dev, + "State changed: %s (From state: [%d] %s =to=> [%d] %s )\n", + di->charge_state == state ? "NO" : "YES", + di->charge_state, + states[di->charge_state], + state, + states[state]); + + di->charge_state = state; +} + +static int abx500_chargalg_check_charger_enable(struct abx500_chargalg *di) +{ + switch (di->charge_state) { + case STATE_NORMAL: + case STATE_MAINTENANCE_A: + case STATE_MAINTENANCE_B: + break; + default: + return 0; + } + + if (di->chg_info.charger_type & USB_CHG) { + return di->usb_chg->ops.check_enable(di->usb_chg, + di->bm->bat_type[di->bm->batt_id].normal_vol_lvl, + di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); + } else if ((di->chg_info.charger_type & AC_CHG) && + !(di->ac_chg->external)) { + return di->ac_chg->ops.check_enable(di->ac_chg, + di->bm->bat_type[di->bm->batt_id].normal_vol_lvl, + di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); + } + return 0; +} + +/** + * abx500_chargalg_check_charger_connection() - Check charger connection change + * @di: pointer to the abx500_chargalg structure + * + * This function will check if there is a change in the charger connection + * and change charge state accordingly. AC has precedence over USB. + */ +static int abx500_chargalg_check_charger_connection(struct abx500_chargalg *di) +{ + if (di->chg_info.conn_chg != di->chg_info.prev_conn_chg || + di->susp_status.suspended_change) { + /* + * Charger state changed or suspension + * has changed since last update + */ + if ((di->chg_info.conn_chg & AC_CHG) && + !di->susp_status.ac_suspended) { + dev_dbg(di->dev, "Charging source is AC\n"); + if (di->chg_info.charger_type != AC_CHG) { + di->chg_info.charger_type = AC_CHG; + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + } + } else if ((di->chg_info.conn_chg & USB_CHG) && + !di->susp_status.usb_suspended) { + dev_dbg(di->dev, "Charging source is USB\n"); + di->chg_info.charger_type = USB_CHG; + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + } else if (di->chg_info.conn_chg && + (di->susp_status.ac_suspended || + di->susp_status.usb_suspended)) { + dev_dbg(di->dev, "Charging is suspended\n"); + di->chg_info.charger_type = NO_CHG; + abx500_chargalg_state_to(di, STATE_SUSPENDED_INIT); + } else { + dev_dbg(di->dev, "Charging source is OFF\n"); + di->chg_info.charger_type = NO_CHG; + abx500_chargalg_state_to(di, STATE_HANDHELD_INIT); + } + di->chg_info.prev_conn_chg = di->chg_info.conn_chg; + di->susp_status.suspended_change = false; + } + return di->chg_info.conn_chg; +} + +/** + * abx500_chargalg_check_current_step_status() - Check charging current + * step status. + * @di: pointer to the abx500_chargalg structure + * + * This function will check if there is a change in the charging current step + * and change charge state accordingly. + */ +static void abx500_chargalg_check_current_step_status + (struct abx500_chargalg *di) +{ + if (di->curr_status.curr_step_change) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + di->curr_status.curr_step_change = false; +} + +/** + * abx500_chargalg_start_safety_timer() - Start charging safety timer + * @di: pointer to the abx500_chargalg structure + * + * The safety timer is used to avoid overcharging of old or bad batteries. + * There are different timers for AC and USB + */ +static void abx500_chargalg_start_safety_timer(struct abx500_chargalg *di) +{ + /* Charger-dependent expiration time in hours*/ + int timer_expiration = 0; + + switch (di->chg_info.charger_type) { + case AC_CHG: + timer_expiration = di->bm->main_safety_tmr_h; + break; + + case USB_CHG: + timer_expiration = di->bm->usb_safety_tmr_h; + break; + + default: + dev_err(di->dev, "Unknown charger to charge from\n"); + break; + } + + di->events.safety_timer_expired = false; + hrtimer_set_expires_range(&di->safety_timer, + ktime_set(timer_expiration * ONE_HOUR_IN_SECONDS, 0), + ktime_set(FIVE_MINUTES_IN_SECONDS, 0)); + hrtimer_start_expires(&di->safety_timer, HRTIMER_MODE_REL); +} + +/** + * abx500_chargalg_stop_safety_timer() - Stop charging safety timer + * @di: pointer to the abx500_chargalg structure + * + * The safety timer is stopped whenever the NORMAL state is exited + */ +static void abx500_chargalg_stop_safety_timer(struct abx500_chargalg *di) +{ + if (hrtimer_try_to_cancel(&di->safety_timer) >= 0) + di->events.safety_timer_expired = false; +} + +/** + * abx500_chargalg_start_maintenance_timer() - Start charging maintenance timer + * @di: pointer to the abx500_chargalg structure + * @duration: duration of ther maintenance timer in hours + * + * The maintenance timer is used to maintain the charge in the battery once + * the battery is considered full. These timers are chosen to match the + * discharge curve of the battery + */ +static void abx500_chargalg_start_maintenance_timer(struct abx500_chargalg *di, + int duration) +{ + hrtimer_set_expires_range(&di->maintenance_timer, + ktime_set(duration * ONE_HOUR_IN_SECONDS, 0), + ktime_set(FIVE_MINUTES_IN_SECONDS, 0)); + di->events.maintenance_timer_expired = false; + hrtimer_start_expires(&di->maintenance_timer, HRTIMER_MODE_REL); +} + +/** + * abx500_chargalg_stop_maintenance_timer() - Stop maintenance timer + * @di: pointer to the abx500_chargalg structure + * + * The maintenance timer is stopped whenever maintenance ends or when another + * state is entered + */ +static void abx500_chargalg_stop_maintenance_timer(struct abx500_chargalg *di) +{ + if (hrtimer_try_to_cancel(&di->maintenance_timer) >= 0) + di->events.maintenance_timer_expired = false; +} + +/** + * abx500_chargalg_kick_watchdog() - Kick charger watchdog + * @di: pointer to the abx500_chargalg structure + * + * The charger watchdog have to be kicked periodically whenever the charger is + * on, else the ABB will reset the system + */ +static int abx500_chargalg_kick_watchdog(struct abx500_chargalg *di) +{ + /* Check if charger exists and kick watchdog if charging */ + if (di->ac_chg && di->ac_chg->ops.kick_wd && + di->chg_info.online_chg & AC_CHG) { + /* + * If AB charger watchdog expired, pm2xxx charging + * gets disabled. To be safe, kick both AB charger watchdog + * and pm2xxx watchdog. + */ + if (di->ac_chg->external && + di->usb_chg && di->usb_chg->ops.kick_wd) + di->usb_chg->ops.kick_wd(di->usb_chg); + + return di->ac_chg->ops.kick_wd(di->ac_chg); + } + else if (di->usb_chg && di->usb_chg->ops.kick_wd && + di->chg_info.online_chg & USB_CHG) + return di->usb_chg->ops.kick_wd(di->usb_chg); + + return -ENXIO; +} + +/** + * abx500_chargalg_ac_en() - Turn on/off the AC charger + * @di: pointer to the abx500_chargalg structure + * @enable: charger on/off + * @vset: requested charger output voltage + * @iset: requested charger output current + * + * The AC charger will be turned on/off with the requested charge voltage and + * current + */ +static int abx500_chargalg_ac_en(struct abx500_chargalg *di, int enable, + int vset, int iset) +{ + static int abx500_chargalg_ex_ac_enable_toggle; + + if (!di->ac_chg || !di->ac_chg->ops.enable) + return -ENXIO; + + /* Select maximum of what both the charger and the battery supports */ + if (di->ac_chg->max_out_volt) + vset = min(vset, di->ac_chg->max_out_volt); + if (di->ac_chg->max_out_curr) + iset = min(iset, di->ac_chg->max_out_curr); + + di->chg_info.ac_iset = iset; + di->chg_info.ac_vset = vset; + + /* Enable external charger */ + if (enable && di->ac_chg->external && + !abx500_chargalg_ex_ac_enable_toggle) { + blocking_notifier_call_chain(&charger_notifier_list, + 0, di->dev); + abx500_chargalg_ex_ac_enable_toggle++; + } + + return di->ac_chg->ops.enable(di->ac_chg, enable, vset, iset); +} + +/** + * abx500_chargalg_usb_en() - Turn on/off the USB charger + * @di: pointer to the abx500_chargalg structure + * @enable: charger on/off + * @vset: requested charger output voltage + * @iset: requested charger output current + * + * The USB charger will be turned on/off with the requested charge voltage and + * current + */ +static int abx500_chargalg_usb_en(struct abx500_chargalg *di, int enable, + int vset, int iset) +{ + if (!di->usb_chg || !di->usb_chg->ops.enable) + return -ENXIO; + + /* Select maximum of what both the charger and the battery supports */ + if (di->usb_chg->max_out_volt) + vset = min(vset, di->usb_chg->max_out_volt); + if (di->usb_chg->max_out_curr) + iset = min(iset, di->usb_chg->max_out_curr); + + di->chg_info.usb_iset = iset; + di->chg_info.usb_vset = vset; + + return di->usb_chg->ops.enable(di->usb_chg, enable, vset, iset); +} + +/** + * abx500_chargalg_update_chg_curr() - Update charger current + * @di: pointer to the abx500_chargalg structure + * @iset: requested charger output current + * + * The charger output current will be updated for the charger + * that is currently in use + */ +static int abx500_chargalg_update_chg_curr(struct abx500_chargalg *di, + int iset) +{ + /* Check if charger exists and update current if charging */ + if (di->ac_chg && di->ac_chg->ops.update_curr && + di->chg_info.charger_type & AC_CHG) { + /* + * Select maximum of what both the charger + * and the battery supports + */ + if (di->ac_chg->max_out_curr) + iset = min(iset, di->ac_chg->max_out_curr); + + di->chg_info.ac_iset = iset; + + return di->ac_chg->ops.update_curr(di->ac_chg, iset); + } else if (di->usb_chg && di->usb_chg->ops.update_curr && + di->chg_info.charger_type & USB_CHG) { + /* + * Select maximum of what both the charger + * and the battery supports + */ + if (di->usb_chg->max_out_curr) + iset = min(iset, di->usb_chg->max_out_curr); + + di->chg_info.usb_iset = iset; + + return di->usb_chg->ops.update_curr(di->usb_chg, iset); + } + + return -ENXIO; +} + +/** + * abx500_chargalg_stop_charging() - Stop charging + * @di: pointer to the abx500_chargalg structure + * + * This function is called from any state where charging should be stopped. + * All charging is disabled and all status parameters and timers are changed + * accordingly + */ +static void abx500_chargalg_stop_charging(struct abx500_chargalg *di) +{ + abx500_chargalg_ac_en(di, false, 0, 0); + abx500_chargalg_usb_en(di, false, 0, 0); + abx500_chargalg_stop_safety_timer(di); + abx500_chargalg_stop_maintenance_timer(di); + di->charge_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + di->maintenance_chg = false; + cancel_delayed_work(&di->chargalg_wd_work); + power_supply_changed(di->chargalg_psy); +} + +/** + * abx500_chargalg_hold_charging() - Pauses charging + * @di: pointer to the abx500_chargalg structure + * + * This function is called in the case where maintenance charging has been + * disabled and instead a battery voltage mode is entered to check when the + * battery voltage has reached a certain recharge voltage + */ +static void abx500_chargalg_hold_charging(struct abx500_chargalg *di) +{ + abx500_chargalg_ac_en(di, false, 0, 0); + abx500_chargalg_usb_en(di, false, 0, 0); + abx500_chargalg_stop_safety_timer(di); + abx500_chargalg_stop_maintenance_timer(di); + di->charge_status = POWER_SUPPLY_STATUS_CHARGING; + di->maintenance_chg = false; + cancel_delayed_work(&di->chargalg_wd_work); + power_supply_changed(di->chargalg_psy); +} + +/** + * abx500_chargalg_start_charging() - Start the charger + * @di: pointer to the abx500_chargalg structure + * @vset: requested charger output voltage + * @iset: requested charger output current + * + * A charger will be enabled depending on the requested charger type that was + * detected previously. + */ +static void abx500_chargalg_start_charging(struct abx500_chargalg *di, + int vset, int iset) +{ + switch (di->chg_info.charger_type) { + case AC_CHG: + dev_dbg(di->dev, + "AC parameters: Vset %d, Ich %d\n", vset, iset); + abx500_chargalg_usb_en(di, false, 0, 0); + abx500_chargalg_ac_en(di, true, vset, iset); + break; + + case USB_CHG: + dev_dbg(di->dev, + "USB parameters: Vset %d, Ich %d\n", vset, iset); + abx500_chargalg_ac_en(di, false, 0, 0); + abx500_chargalg_usb_en(di, true, vset, iset); + break; + + default: + dev_err(di->dev, "Unknown charger to charge from\n"); + break; + } +} + +/** + * abx500_chargalg_check_temp() - Check battery temperature ranges + * @di: pointer to the abx500_chargalg structure + * + * The battery temperature is checked against the predefined limits and the + * charge state is changed accordingly + */ +static void abx500_chargalg_check_temp(struct abx500_chargalg *di) +{ + if (di->batt_data.temp > (di->bm->temp_low + di->t_hyst_norm) && + di->batt_data.temp < (di->bm->temp_high - di->t_hyst_norm)) { + /* Temp OK! */ + di->events.btemp_underover = false; + di->events.btemp_lowhigh = false; + di->t_hyst_norm = 0; + di->t_hyst_lowhigh = 0; + } else { + if (((di->batt_data.temp >= di->bm->temp_high) && + (di->batt_data.temp < + (di->bm->temp_over - di->t_hyst_lowhigh))) || + ((di->batt_data.temp > + (di->bm->temp_under + di->t_hyst_lowhigh)) && + (di->batt_data.temp <= di->bm->temp_low))) { + /* TEMP minor!!!!! */ + di->events.btemp_underover = false; + di->events.btemp_lowhigh = true; + di->t_hyst_norm = di->bm->temp_hysteresis; + di->t_hyst_lowhigh = 0; + } else if (di->batt_data.temp <= di->bm->temp_under || + di->batt_data.temp >= di->bm->temp_over) { + /* TEMP major!!!!! */ + di->events.btemp_underover = true; + di->events.btemp_lowhigh = false; + di->t_hyst_norm = 0; + di->t_hyst_lowhigh = di->bm->temp_hysteresis; + } else { + /* Within hysteresis */ + dev_dbg(di->dev, "Within hysteresis limit temp: %d " + "hyst_lowhigh %d, hyst normal %d\n", + di->batt_data.temp, di->t_hyst_lowhigh, + di->t_hyst_norm); + } + } +} + +/** + * abx500_chargalg_check_charger_voltage() - Check charger voltage + * @di: pointer to the abx500_chargalg structure + * + * Charger voltage is checked against maximum limit + */ +static void abx500_chargalg_check_charger_voltage(struct abx500_chargalg *di) +{ + if (di->chg_info.usb_volt > di->bm->chg_params->usb_volt_max) + di->chg_info.usb_chg_ok = false; + else + di->chg_info.usb_chg_ok = true; + + if (di->chg_info.ac_volt > di->bm->chg_params->ac_volt_max) + di->chg_info.ac_chg_ok = false; + else + di->chg_info.ac_chg_ok = true; + +} + +/** + * abx500_chargalg_end_of_charge() - Check if end-of-charge criteria is fulfilled + * @di: pointer to the abx500_chargalg structure + * + * End-of-charge criteria is fulfilled when the battery voltage is above a + * certain limit and the battery current is below a certain limit for a + * predefined number of consecutive seconds. If true, the battery is full + */ +static void abx500_chargalg_end_of_charge(struct abx500_chargalg *di) +{ + if (di->charge_status == POWER_SUPPLY_STATUS_CHARGING && + di->charge_state == STATE_NORMAL && + !di->maintenance_chg && (di->batt_data.volt >= + di->bm->bat_type[di->bm->batt_id].termination_vol || + di->events.usb_cv_active || di->events.ac_cv_active) && + di->batt_data.avg_curr < + di->bm->bat_type[di->bm->batt_id].termination_curr && + di->batt_data.avg_curr > 0) { + if (++di->eoc_cnt >= EOC_COND_CNT) { + di->eoc_cnt = 0; + di->charge_status = POWER_SUPPLY_STATUS_FULL; + di->maintenance_chg = true; + dev_dbg(di->dev, "EOC reached!\n"); + power_supply_changed(di->chargalg_psy); + } else { + dev_dbg(di->dev, + " EOC limit reached for the %d" + " time, out of %d before EOC\n", + di->eoc_cnt, + EOC_COND_CNT); + } + } else { + di->eoc_cnt = 0; + } +} + +static void init_maxim_chg_curr(struct abx500_chargalg *di) +{ + di->ccm.original_iset = + di->bm->bat_type[di->bm->batt_id].normal_cur_lvl; + di->ccm.current_iset = + di->bm->bat_type[di->bm->batt_id].normal_cur_lvl; + di->ccm.test_delta_i = di->bm->maxi->charger_curr_step; + di->ccm.max_current = di->bm->maxi->chg_curr; + di->ccm.condition_cnt = di->bm->maxi->wait_cycles; + di->ccm.level = 0; +} + +/** + * abx500_chargalg_chg_curr_maxim - increases the charger current to + * compensate for the system load + * @di pointer to the abx500_chargalg structure + * + * This maximization function is used to raise the charger current to get the + * battery current as close to the optimal value as possible. The battery + * current during charging is affected by the system load + */ +static enum maxim_ret abx500_chargalg_chg_curr_maxim(struct abx500_chargalg *di) +{ + int delta_i; + + if (!di->bm->maxi->ena_maxi) + return MAXIM_RET_NOACTION; + + delta_i = di->ccm.original_iset - di->batt_data.inst_curr; + + if (di->events.vbus_collapsed) { + dev_dbg(di->dev, "Charger voltage has collapsed %d\n", + di->ccm.wait_cnt); + if (di->ccm.wait_cnt == 0) { + dev_dbg(di->dev, "lowering current\n"); + di->ccm.wait_cnt++; + di->ccm.condition_cnt = di->bm->maxi->wait_cycles; + di->ccm.max_current = + di->ccm.current_iset - di->ccm.test_delta_i; + di->ccm.current_iset = di->ccm.max_current; + di->ccm.level--; + return MAXIM_RET_CHANGE; + } else { + dev_dbg(di->dev, "waiting\n"); + /* Let's go in here twice before lowering curr again */ + di->ccm.wait_cnt = (di->ccm.wait_cnt + 1) % 3; + return MAXIM_RET_NOACTION; + } + } + + di->ccm.wait_cnt = 0; + + if ((di->batt_data.inst_curr > di->ccm.original_iset)) { + dev_dbg(di->dev, " Maximization Ibat (%dmA) too high" + " (limit %dmA) (current iset: %dmA)!\n", + di->batt_data.inst_curr, di->ccm.original_iset, + di->ccm.current_iset); + + if (di->ccm.current_iset == di->ccm.original_iset) + return MAXIM_RET_NOACTION; + + di->ccm.condition_cnt = di->bm->maxi->wait_cycles; + di->ccm.current_iset = di->ccm.original_iset; + di->ccm.level = 0; + + return MAXIM_RET_IBAT_TOO_HIGH; + } + + if (delta_i > di->ccm.test_delta_i && + (di->ccm.current_iset + di->ccm.test_delta_i) < + di->ccm.max_current) { + if (di->ccm.condition_cnt-- == 0) { + /* Increse the iset with cco.test_delta_i */ + di->ccm.condition_cnt = di->bm->maxi->wait_cycles; + di->ccm.current_iset += di->ccm.test_delta_i; + di->ccm.level++; + dev_dbg(di->dev, " Maximization needed, increase" + " with %d mA to %dmA (Optimal ibat: %d)" + " Level %d\n", + di->ccm.test_delta_i, + di->ccm.current_iset, + di->ccm.original_iset, + di->ccm.level); + return MAXIM_RET_CHANGE; + } else { + return MAXIM_RET_NOACTION; + } + } else { + di->ccm.condition_cnt = di->bm->maxi->wait_cycles; + return MAXIM_RET_NOACTION; + } +} + +static void handle_maxim_chg_curr(struct abx500_chargalg *di) +{ + enum maxim_ret ret; + int result; + + ret = abx500_chargalg_chg_curr_maxim(di); + switch (ret) { + case MAXIM_RET_CHANGE: + result = abx500_chargalg_update_chg_curr(di, + di->ccm.current_iset); + if (result) + dev_err(di->dev, "failed to set chg curr\n"); + break; + case MAXIM_RET_IBAT_TOO_HIGH: + result = abx500_chargalg_update_chg_curr(di, + di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); + if (result) + dev_err(di->dev, "failed to set chg curr\n"); + break; + + case MAXIM_RET_NOACTION: + default: + /* Do nothing..*/ + break; + } +} + +static int abx500_chargalg_get_ext_psy_data(struct device *dev, void *data) +{ + struct power_supply *psy; + struct power_supply *ext = dev_get_drvdata(dev); + const char **supplicants = (const char **)ext->supplied_to; + struct abx500_chargalg *di; + union power_supply_propval ret; + int j; + bool capacity_updated = false; + + psy = (struct power_supply *)data; + di = power_supply_get_drvdata(psy); + /* For all psy where the driver name appears in any supplied_to */ + j = match_string(supplicants, ext->num_supplicants, psy->desc->name); + if (j < 0) + return 0; + + /* + * If external is not registering 'POWER_SUPPLY_PROP_CAPACITY' to its + * property because of handling that sysfs entry on its own, this is + * the place to get the battery capacity. + */ + if (!power_supply_get_property(ext, POWER_SUPPLY_PROP_CAPACITY, &ret)) { + di->batt_data.percent = ret.intval; + capacity_updated = true; + } + + /* Go through all properties for the psy */ + for (j = 0; j < ext->desc->num_properties; j++) { + enum power_supply_property prop; + prop = ext->desc->properties[j]; + + /* + * Initialize chargers if not already done. + * The ab8500_charger*/ + if (!di->ac_chg && + ext->desc->type == POWER_SUPPLY_TYPE_MAINS) + di->ac_chg = psy_to_ux500_charger(ext); + else if (!di->usb_chg && + ext->desc->type == POWER_SUPPLY_TYPE_USB) + di->usb_chg = psy_to_ux500_charger(ext); + + if (power_supply_get_property(ext, prop, &ret)) + continue; + switch (prop) { + case POWER_SUPPLY_PROP_PRESENT: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + /* Battery present */ + if (ret.intval) + di->events.batt_rem = false; + /* Battery removed */ + else + di->events.batt_rem = true; + break; + case POWER_SUPPLY_TYPE_MAINS: + /* AC disconnected */ + if (!ret.intval && + (di->chg_info.conn_chg & AC_CHG)) { + di->chg_info.prev_conn_chg = + di->chg_info.conn_chg; + di->chg_info.conn_chg &= ~AC_CHG; + } + /* AC connected */ + else if (ret.intval && + !(di->chg_info.conn_chg & AC_CHG)) { + di->chg_info.prev_conn_chg = + di->chg_info.conn_chg; + di->chg_info.conn_chg |= AC_CHG; + } + break; + case POWER_SUPPLY_TYPE_USB: + /* USB disconnected */ + if (!ret.intval && + (di->chg_info.conn_chg & USB_CHG)) { + di->chg_info.prev_conn_chg = + di->chg_info.conn_chg; + di->chg_info.conn_chg &= ~USB_CHG; + } + /* USB connected */ + else if (ret.intval && + !(di->chg_info.conn_chg & USB_CHG)) { + di->chg_info.prev_conn_chg = + di->chg_info.conn_chg; + di->chg_info.conn_chg |= USB_CHG; + } + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_ONLINE: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + break; + case POWER_SUPPLY_TYPE_MAINS: + /* AC offline */ + if (!ret.intval && + (di->chg_info.online_chg & AC_CHG)) { + di->chg_info.prev_online_chg = + di->chg_info.online_chg; + di->chg_info.online_chg &= ~AC_CHG; + } + /* AC online */ + else if (ret.intval && + !(di->chg_info.online_chg & AC_CHG)) { + di->chg_info.prev_online_chg = + di->chg_info.online_chg; + di->chg_info.online_chg |= AC_CHG; + queue_delayed_work(di->chargalg_wq, + &di->chargalg_wd_work, 0); + } + break; + case POWER_SUPPLY_TYPE_USB: + /* USB offline */ + if (!ret.intval && + (di->chg_info.online_chg & USB_CHG)) { + di->chg_info.prev_online_chg = + di->chg_info.online_chg; + di->chg_info.online_chg &= ~USB_CHG; + } + /* USB online */ + else if (ret.intval && + !(di->chg_info.online_chg & USB_CHG)) { + di->chg_info.prev_online_chg = + di->chg_info.online_chg; + di->chg_info.online_chg |= USB_CHG; + queue_delayed_work(di->chargalg_wq, + &di->chargalg_wd_work, 0); + } + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_HEALTH: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + break; + case POWER_SUPPLY_TYPE_MAINS: + switch (ret.intval) { + case POWER_SUPPLY_HEALTH_UNSPEC_FAILURE: + di->events.mainextchnotok = true; + di->events.main_thermal_prot = false; + di->events.main_ovv = false; + di->events.ac_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_DEAD: + di->events.ac_wd_expired = true; + di->events.mainextchnotok = false; + di->events.main_ovv = false; + di->events.main_thermal_prot = false; + break; + case POWER_SUPPLY_HEALTH_COLD: + case POWER_SUPPLY_HEALTH_OVERHEAT: + di->events.main_thermal_prot = true; + di->events.mainextchnotok = false; + di->events.main_ovv = false; + di->events.ac_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_OVERVOLTAGE: + di->events.main_ovv = true; + di->events.mainextchnotok = false; + di->events.main_thermal_prot = false; + di->events.ac_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_GOOD: + di->events.main_thermal_prot = false; + di->events.mainextchnotok = false; + di->events.main_ovv = false; + di->events.ac_wd_expired = false; + break; + default: + break; + } + break; + + case POWER_SUPPLY_TYPE_USB: + switch (ret.intval) { + case POWER_SUPPLY_HEALTH_UNSPEC_FAILURE: + di->events.usbchargernotok = true; + di->events.usb_thermal_prot = false; + di->events.vbus_ovv = false; + di->events.usb_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_DEAD: + di->events.usb_wd_expired = true; + di->events.usbchargernotok = false; + di->events.usb_thermal_prot = false; + di->events.vbus_ovv = false; + break; + case POWER_SUPPLY_HEALTH_COLD: + case POWER_SUPPLY_HEALTH_OVERHEAT: + di->events.usb_thermal_prot = true; + di->events.usbchargernotok = false; + di->events.vbus_ovv = false; + di->events.usb_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_OVERVOLTAGE: + di->events.vbus_ovv = true; + di->events.usbchargernotok = false; + di->events.usb_thermal_prot = false; + di->events.usb_wd_expired = false; + break; + case POWER_SUPPLY_HEALTH_GOOD: + di->events.usbchargernotok = false; + di->events.usb_thermal_prot = false; + di->events.vbus_ovv = false; + di->events.usb_wd_expired = false; + break; + default: + break; + } + default: + break; + } + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + di->batt_data.volt = ret.intval / 1000; + break; + case POWER_SUPPLY_TYPE_MAINS: + di->chg_info.ac_volt = ret.intval / 1000; + break; + case POWER_SUPPLY_TYPE_USB: + di->chg_info.usb_volt = ret.intval / 1000; + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_MAINS: + /* AVG is used to indicate when we are + * in CV mode */ + if (ret.intval) + di->events.ac_cv_active = true; + else + di->events.ac_cv_active = false; + + break; + case POWER_SUPPLY_TYPE_USB: + /* AVG is used to indicate when we are + * in CV mode */ + if (ret.intval) + di->events.usb_cv_active = true; + else + di->events.usb_cv_active = false; + + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_TECHNOLOGY: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + if (ret.intval) + di->events.batt_unknown = false; + else + di->events.batt_unknown = true; + + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_TEMP: + di->batt_data.temp = ret.intval / 10; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_MAINS: + di->chg_info.ac_curr = + ret.intval / 1000; + break; + case POWER_SUPPLY_TYPE_USB: + di->chg_info.usb_curr = + ret.intval / 1000; + break; + case POWER_SUPPLY_TYPE_BATTERY: + di->batt_data.inst_curr = ret.intval / 1000; + break; + default: + break; + } + break; + + case POWER_SUPPLY_PROP_CURRENT_AVG: + switch (ext->desc->type) { + case POWER_SUPPLY_TYPE_BATTERY: + di->batt_data.avg_curr = ret.intval / 1000; + break; + case POWER_SUPPLY_TYPE_USB: + if (ret.intval) + di->events.vbus_collapsed = true; + else + di->events.vbus_collapsed = false; + break; + default: + break; + } + break; + case POWER_SUPPLY_PROP_CAPACITY: + if (!capacity_updated) + di->batt_data.percent = ret.intval; + break; + default: + break; + } + } + return 0; +} + +/** + * abx500_chargalg_external_power_changed() - callback for power supply changes + * @psy: pointer to the structure power_supply + * + * This function is the entry point of the pointer external_power_changed + * of the structure power_supply. + * This function gets executed when there is a change in any external power + * supply that this driver needs to be notified of. + */ +static void abx500_chargalg_external_power_changed(struct power_supply *psy) +{ + struct abx500_chargalg *di = power_supply_get_drvdata(psy); + + /* + * Trigger execution of the algorithm instantly and read + * all power_supply properties there instead + */ + queue_work(di->chargalg_wq, &di->chargalg_work); +} + +/** + * abx500_chargalg_algorithm() - Main function for the algorithm + * @di: pointer to the abx500_chargalg structure + * + * This is the main control function for the charging algorithm. + * It is called periodically or when something happens that will + * trigger a state change + */ +static void abx500_chargalg_algorithm(struct abx500_chargalg *di) +{ + int charger_status; + int ret; + int curr_step_lvl; + + /* Collect data from all power_supply class devices */ + class_for_each_device(power_supply_class, NULL, + di->chargalg_psy, abx500_chargalg_get_ext_psy_data); + + abx500_chargalg_end_of_charge(di); + abx500_chargalg_check_temp(di); + abx500_chargalg_check_charger_voltage(di); + + charger_status = abx500_chargalg_check_charger_connection(di); + abx500_chargalg_check_current_step_status(di); + + if (is_ab8500(di->parent)) { + ret = abx500_chargalg_check_charger_enable(di); + if (ret < 0) + dev_err(di->dev, "Checking charger is enabled error" + ": Returned Value %d\n", ret); + } + + /* + * First check if we have a charger connected. + * Also we don't allow charging of unknown batteries if configured + * this way + */ + if (!charger_status || + (di->events.batt_unknown && !di->bm->chg_unknown_bat)) { + if (di->charge_state != STATE_HANDHELD) { + di->events.safety_timer_expired = false; + abx500_chargalg_state_to(di, STATE_HANDHELD_INIT); + } + } + + /* If suspended, we should not continue checking the flags */ + else if (di->charge_state == STATE_SUSPENDED_INIT || + di->charge_state == STATE_SUSPENDED) { + /* We don't do anything here, just don,t continue */ + } + + /* Safety timer expiration */ + else if (di->events.safety_timer_expired) { + if (di->charge_state != STATE_SAFETY_TIMER_EXPIRED) + abx500_chargalg_state_to(di, + STATE_SAFETY_TIMER_EXPIRED_INIT); + } + /* + * Check if any interrupts has occured + * that will prevent us from charging + */ + + /* Battery removed */ + else if (di->events.batt_rem) { + if (di->charge_state != STATE_BATT_REMOVED) + abx500_chargalg_state_to(di, STATE_BATT_REMOVED_INIT); + } + /* Main or USB charger not ok. */ + else if (di->events.mainextchnotok || di->events.usbchargernotok) { + /* + * If vbus_collapsed is set, we have to lower the charger + * current, which is done in the normal state below + */ + if (di->charge_state != STATE_CHG_NOT_OK && + !di->events.vbus_collapsed) + abx500_chargalg_state_to(di, STATE_CHG_NOT_OK_INIT); + } + /* VBUS, Main or VBAT OVV. */ + else if (di->events.vbus_ovv || + di->events.main_ovv || + di->events.batt_ovv || + !di->chg_info.usb_chg_ok || + !di->chg_info.ac_chg_ok) { + if (di->charge_state != STATE_OVV_PROTECT) + abx500_chargalg_state_to(di, STATE_OVV_PROTECT_INIT); + } + /* USB Thermal, stop charging */ + else if (di->events.main_thermal_prot || + di->events.usb_thermal_prot) { + if (di->charge_state != STATE_HW_TEMP_PROTECT) + abx500_chargalg_state_to(di, + STATE_HW_TEMP_PROTECT_INIT); + } + /* Battery temp over/under */ + else if (di->events.btemp_underover) { + if (di->charge_state != STATE_TEMP_UNDEROVER) + abx500_chargalg_state_to(di, + STATE_TEMP_UNDEROVER_INIT); + } + /* Watchdog expired */ + else if (di->events.ac_wd_expired || + di->events.usb_wd_expired) { + if (di->charge_state != STATE_WD_EXPIRED) + abx500_chargalg_state_to(di, STATE_WD_EXPIRED_INIT); + } + /* Battery temp high/low */ + else if (di->events.btemp_lowhigh) { + if (di->charge_state != STATE_TEMP_LOWHIGH) + abx500_chargalg_state_to(di, STATE_TEMP_LOWHIGH_INIT); + } + + dev_dbg(di->dev, + "[CHARGALG] Vb %d Ib_avg %d Ib_inst %d Tb %d Cap %d Maint %d " + "State %s Active_chg %d Chg_status %d AC %d USB %d " + "AC_online %d USB_online %d AC_CV %d USB_CV %d AC_I %d " + "USB_I %d AC_Vset %d AC_Iset %d USB_Vset %d USB_Iset %d\n", + di->batt_data.volt, + di->batt_data.avg_curr, + di->batt_data.inst_curr, + di->batt_data.temp, + di->batt_data.percent, + di->maintenance_chg, + states[di->charge_state], + di->chg_info.charger_type, + di->charge_status, + di->chg_info.conn_chg & AC_CHG, + di->chg_info.conn_chg & USB_CHG, + di->chg_info.online_chg & AC_CHG, + di->chg_info.online_chg & USB_CHG, + di->events.ac_cv_active, + di->events.usb_cv_active, + di->chg_info.ac_curr, + di->chg_info.usb_curr, + di->chg_info.ac_vset, + di->chg_info.ac_iset, + di->chg_info.usb_vset, + di->chg_info.usb_iset); + + switch (di->charge_state) { + case STATE_HANDHELD_INIT: + abx500_chargalg_stop_charging(di); + di->charge_status = POWER_SUPPLY_STATUS_DISCHARGING; + abx500_chargalg_state_to(di, STATE_HANDHELD); + fallthrough; + + case STATE_HANDHELD: + break; + + case STATE_SUSPENDED_INIT: + if (di->susp_status.ac_suspended) + abx500_chargalg_ac_en(di, false, 0, 0); + if (di->susp_status.usb_suspended) + abx500_chargalg_usb_en(di, false, 0, 0); + abx500_chargalg_stop_safety_timer(di); + abx500_chargalg_stop_maintenance_timer(di); + di->charge_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + di->maintenance_chg = false; + abx500_chargalg_state_to(di, STATE_SUSPENDED); + power_supply_changed(di->chargalg_psy); + fallthrough; + + case STATE_SUSPENDED: + /* CHARGING is suspended */ + break; + + case STATE_BATT_REMOVED_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_BATT_REMOVED); + fallthrough; + + case STATE_BATT_REMOVED: + if (!di->events.batt_rem) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_HW_TEMP_PROTECT_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_HW_TEMP_PROTECT); + fallthrough; + + case STATE_HW_TEMP_PROTECT: + if (!di->events.main_thermal_prot && + !di->events.usb_thermal_prot) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_OVV_PROTECT_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_OVV_PROTECT); + fallthrough; + + case STATE_OVV_PROTECT: + if (!di->events.vbus_ovv && + !di->events.main_ovv && + !di->events.batt_ovv && + di->chg_info.usb_chg_ok && + di->chg_info.ac_chg_ok) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_CHG_NOT_OK_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_CHG_NOT_OK); + fallthrough; + + case STATE_CHG_NOT_OK: + if (!di->events.mainextchnotok && + !di->events.usbchargernotok) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_SAFETY_TIMER_EXPIRED_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_SAFETY_TIMER_EXPIRED); + fallthrough; + + case STATE_SAFETY_TIMER_EXPIRED: + /* We exit this state when charger is removed */ + break; + + case STATE_NORMAL_INIT: + if (di->curr_status.curr_step == CHARGALG_CURR_STEP_LOW) + abx500_chargalg_stop_charging(di); + else { + curr_step_lvl = di->bm->bat_type[ + di->bm->batt_id].normal_cur_lvl + * di->curr_status.curr_step + / CHARGALG_CURR_STEP_HIGH; + abx500_chargalg_start_charging(di, + di->bm->bat_type[di->bm->batt_id] + .normal_vol_lvl, curr_step_lvl); + } + + abx500_chargalg_state_to(di, STATE_NORMAL); + abx500_chargalg_start_safety_timer(di); + abx500_chargalg_stop_maintenance_timer(di); + init_maxim_chg_curr(di); + di->charge_status = POWER_SUPPLY_STATUS_CHARGING; + di->eoc_cnt = 0; + di->maintenance_chg = false; + power_supply_changed(di->chargalg_psy); + + break; + + case STATE_NORMAL: + handle_maxim_chg_curr(di); + if (di->charge_status == POWER_SUPPLY_STATUS_FULL && + di->maintenance_chg) { + if (di->bm->no_maintenance) + abx500_chargalg_state_to(di, + STATE_WAIT_FOR_RECHARGE_INIT); + else + abx500_chargalg_state_to(di, + STATE_MAINTENANCE_A_INIT); + } + break; + + /* This state will be used when the maintenance state is disabled */ + case STATE_WAIT_FOR_RECHARGE_INIT: + abx500_chargalg_hold_charging(di); + abx500_chargalg_state_to(di, STATE_WAIT_FOR_RECHARGE); + fallthrough; + + case STATE_WAIT_FOR_RECHARGE: + if (di->batt_data.percent <= + di->bm->bat_type[di->bm->batt_id]. + recharge_cap) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_MAINTENANCE_A_INIT: + abx500_chargalg_stop_safety_timer(di); + abx500_chargalg_start_maintenance_timer(di, + di->bm->bat_type[ + di->bm->batt_id].maint_a_chg_timer_h); + abx500_chargalg_start_charging(di, + di->bm->bat_type[ + di->bm->batt_id].maint_a_vol_lvl, + di->bm->bat_type[ + di->bm->batt_id].maint_a_cur_lvl); + abx500_chargalg_state_to(di, STATE_MAINTENANCE_A); + power_supply_changed(di->chargalg_psy); + fallthrough; + + case STATE_MAINTENANCE_A: + if (di->events.maintenance_timer_expired) { + abx500_chargalg_stop_maintenance_timer(di); + abx500_chargalg_state_to(di, STATE_MAINTENANCE_B_INIT); + } + break; + + case STATE_MAINTENANCE_B_INIT: + abx500_chargalg_start_maintenance_timer(di, + di->bm->bat_type[ + di->bm->batt_id].maint_b_chg_timer_h); + abx500_chargalg_start_charging(di, + di->bm->bat_type[ + di->bm->batt_id].maint_b_vol_lvl, + di->bm->bat_type[ + di->bm->batt_id].maint_b_cur_lvl); + abx500_chargalg_state_to(di, STATE_MAINTENANCE_B); + power_supply_changed(di->chargalg_psy); + fallthrough; + + case STATE_MAINTENANCE_B: + if (di->events.maintenance_timer_expired) { + abx500_chargalg_stop_maintenance_timer(di); + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + } + break; + + case STATE_TEMP_LOWHIGH_INIT: + abx500_chargalg_start_charging(di, + di->bm->bat_type[ + di->bm->batt_id].low_high_vol_lvl, + di->bm->bat_type[ + di->bm->batt_id].low_high_cur_lvl); + abx500_chargalg_stop_maintenance_timer(di); + di->charge_status = POWER_SUPPLY_STATUS_CHARGING; + abx500_chargalg_state_to(di, STATE_TEMP_LOWHIGH); + power_supply_changed(di->chargalg_psy); + fallthrough; + + case STATE_TEMP_LOWHIGH: + if (!di->events.btemp_lowhigh) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_WD_EXPIRED_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_WD_EXPIRED); + fallthrough; + + case STATE_WD_EXPIRED: + if (!di->events.ac_wd_expired && + !di->events.usb_wd_expired) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + + case STATE_TEMP_UNDEROVER_INIT: + abx500_chargalg_stop_charging(di); + abx500_chargalg_state_to(di, STATE_TEMP_UNDEROVER); + fallthrough; + + case STATE_TEMP_UNDEROVER: + if (!di->events.btemp_underover) + abx500_chargalg_state_to(di, STATE_NORMAL_INIT); + break; + } + + /* Start charging directly if the new state is a charge state */ + if (di->charge_state == STATE_NORMAL_INIT || + di->charge_state == STATE_MAINTENANCE_A_INIT || + di->charge_state == STATE_MAINTENANCE_B_INIT) + queue_work(di->chargalg_wq, &di->chargalg_work); +} + +/** + * abx500_chargalg_periodic_work() - Periodic work for the algorithm + * @work: pointer to the work_struct structure + * + * Work queue function for the charging algorithm + */ +static void abx500_chargalg_periodic_work(struct work_struct *work) +{ + struct abx500_chargalg *di = container_of(work, + struct abx500_chargalg, chargalg_periodic_work.work); + + abx500_chargalg_algorithm(di); + + /* + * If a charger is connected then the battery has to be monitored + * frequently, else the work can be delayed. + */ + if (di->chg_info.conn_chg) + queue_delayed_work(di->chargalg_wq, + &di->chargalg_periodic_work, + di->bm->interval_charging * HZ); + else + queue_delayed_work(di->chargalg_wq, + &di->chargalg_periodic_work, + di->bm->interval_not_charging * HZ); +} + +/** + * abx500_chargalg_wd_work() - periodic work to kick the charger watchdog + * @work: pointer to the work_struct structure + * + * Work queue function for kicking the charger watchdog + */ +static void abx500_chargalg_wd_work(struct work_struct *work) +{ + int ret; + struct abx500_chargalg *di = container_of(work, + struct abx500_chargalg, chargalg_wd_work.work); + + dev_dbg(di->dev, "abx500_chargalg_wd_work\n"); + + ret = abx500_chargalg_kick_watchdog(di); + if (ret < 0) + dev_err(di->dev, "failed to kick watchdog\n"); + + queue_delayed_work(di->chargalg_wq, + &di->chargalg_wd_work, CHG_WD_INTERVAL); +} + +/** + * abx500_chargalg_work() - Work to run the charging algorithm instantly + * @work: pointer to the work_struct structure + * + * Work queue function for calling the charging algorithm + */ +static void abx500_chargalg_work(struct work_struct *work) +{ + struct abx500_chargalg *di = container_of(work, + struct abx500_chargalg, chargalg_work); + + abx500_chargalg_algorithm(di); +} + +/** + * abx500_chargalg_get_property() - get the chargalg properties + * @psy: pointer to the power_supply structure + * @psp: pointer to the power_supply_property structure + * @val: pointer to the power_supply_propval union + * + * This function gets called when an application tries to get the + * chargalg properties by reading the sysfs files. + * status: charging/discharging/full/unknown + * health: health of the battery + * Returns error code in case of failure else 0 on success + */ +static int abx500_chargalg_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct abx500_chargalg *di = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = di->charge_status; + break; + case POWER_SUPPLY_PROP_HEALTH: + if (di->events.batt_ovv) { + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + } else if (di->events.btemp_underover) { + if (di->batt_data.temp <= di->bm->temp_under) + val->intval = POWER_SUPPLY_HEALTH_COLD; + else + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + } else if (di->charge_state == STATE_SAFETY_TIMER_EXPIRED || + di->charge_state == STATE_SAFETY_TIMER_EXPIRED_INIT) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + } else { + val->intval = POWER_SUPPLY_HEALTH_GOOD; + } + break; + default: + return -EINVAL; + } + return 0; +} + +/* Exposure to the sysfs interface */ + +static ssize_t abx500_chargalg_curr_step_show(struct abx500_chargalg *di, + char *buf) +{ + return sprintf(buf, "%d\n", di->curr_status.curr_step); +} + +static ssize_t abx500_chargalg_curr_step_store(struct abx500_chargalg *di, + const char *buf, size_t length) +{ + long int param; + int ret; + + ret = kstrtol(buf, 10, ¶m); + if (ret < 0) + return ret; + + di->curr_status.curr_step = param; + if (di->curr_status.curr_step >= CHARGALG_CURR_STEP_LOW && + di->curr_status.curr_step <= CHARGALG_CURR_STEP_HIGH) { + di->curr_status.curr_step_change = true; + queue_work(di->chargalg_wq, &di->chargalg_work); + } else + dev_info(di->dev, "Wrong current step\n" + "Enter 0. Disable AC/USB Charging\n" + "1--100. Set AC/USB charging current step\n" + "100. Enable AC/USB Charging\n"); + + return strlen(buf); +} + + +static ssize_t abx500_chargalg_en_show(struct abx500_chargalg *di, + char *buf) +{ + return sprintf(buf, "%d\n", + di->susp_status.ac_suspended && + di->susp_status.usb_suspended); +} + +static ssize_t abx500_chargalg_en_store(struct abx500_chargalg *di, + const char *buf, size_t length) +{ + long int param; + int ac_usb; + int ret; + + ret = kstrtol(buf, 10, ¶m); + if (ret < 0) + return ret; + + ac_usb = param; + switch (ac_usb) { + case 0: + /* Disable charging */ + di->susp_status.ac_suspended = true; + di->susp_status.usb_suspended = true; + di->susp_status.suspended_change = true; + /* Trigger a state change */ + queue_work(di->chargalg_wq, + &di->chargalg_work); + break; + case 1: + /* Enable AC Charging */ + di->susp_status.ac_suspended = false; + di->susp_status.suspended_change = true; + /* Trigger a state change */ + queue_work(di->chargalg_wq, + &di->chargalg_work); + break; + case 2: + /* Enable USB charging */ + di->susp_status.usb_suspended = false; + di->susp_status.suspended_change = true; + /* Trigger a state change */ + queue_work(di->chargalg_wq, + &di->chargalg_work); + break; + default: + dev_info(di->dev, "Wrong input\n" + "Enter 0. Disable AC/USB Charging\n" + "1. Enable AC charging\n" + "2. Enable USB Charging\n"); + } + return strlen(buf); +} + +static struct abx500_chargalg_sysfs_entry abx500_chargalg_en_charger = + __ATTR(chargalg, 0644, abx500_chargalg_en_show, + abx500_chargalg_en_store); + +static struct abx500_chargalg_sysfs_entry abx500_chargalg_curr_step = + __ATTR(chargalg_curr_step, 0644, abx500_chargalg_curr_step_show, + abx500_chargalg_curr_step_store); + +static ssize_t abx500_chargalg_sysfs_show(struct kobject *kobj, + struct attribute *attr, char *buf) +{ + struct abx500_chargalg_sysfs_entry *entry = container_of(attr, + struct abx500_chargalg_sysfs_entry, attr); + + struct abx500_chargalg *di = container_of(kobj, + struct abx500_chargalg, chargalg_kobject); + + if (!entry->show) + return -EIO; + + return entry->show(di, buf); +} + +static ssize_t abx500_chargalg_sysfs_charger(struct kobject *kobj, + struct attribute *attr, const char *buf, size_t length) +{ + struct abx500_chargalg_sysfs_entry *entry = container_of(attr, + struct abx500_chargalg_sysfs_entry, attr); + + struct abx500_chargalg *di = container_of(kobj, + struct abx500_chargalg, chargalg_kobject); + + if (!entry->store) + return -EIO; + + return entry->store(di, buf, length); +} + +static struct attribute *abx500_chargalg_chg[] = { + &abx500_chargalg_en_charger.attr, + &abx500_chargalg_curr_step.attr, + NULL, +}; + +static const struct sysfs_ops abx500_chargalg_sysfs_ops = { + .show = abx500_chargalg_sysfs_show, + .store = abx500_chargalg_sysfs_charger, +}; + +static struct kobj_type abx500_chargalg_ktype = { + .sysfs_ops = &abx500_chargalg_sysfs_ops, + .default_attrs = abx500_chargalg_chg, +}; + +/** + * abx500_chargalg_sysfs_exit() - de-init of sysfs entry + * @di: pointer to the struct abx500_chargalg + * + * This function removes the entry in sysfs. + */ +static void abx500_chargalg_sysfs_exit(struct abx500_chargalg *di) +{ + kobject_del(&di->chargalg_kobject); +} + +/** + * abx500_chargalg_sysfs_init() - init of sysfs entry + * @di: pointer to the struct abx500_chargalg + * + * This function adds an entry in sysfs. + * Returns error code in case of failure else 0(on success) + */ +static int abx500_chargalg_sysfs_init(struct abx500_chargalg *di) +{ + int ret = 0; + + ret = kobject_init_and_add(&di->chargalg_kobject, + &abx500_chargalg_ktype, + NULL, "abx500_chargalg"); + if (ret < 0) + dev_err(di->dev, "failed to create sysfs entry\n"); + + return ret; +} +/* Exposure to the sysfs interface <<END>> */ + +#if defined(CONFIG_PM) +static int abx500_chargalg_resume(struct platform_device *pdev) +{ + struct abx500_chargalg *di = platform_get_drvdata(pdev); + + /* Kick charger watchdog if charging (any charger online) */ + if (di->chg_info.online_chg) + queue_delayed_work(di->chargalg_wq, &di->chargalg_wd_work, 0); + + /* + * Run the charging algorithm directly to be sure we don't + * do it too seldom + */ + queue_delayed_work(di->chargalg_wq, &di->chargalg_periodic_work, 0); + + return 0; +} + +static int abx500_chargalg_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct abx500_chargalg *di = platform_get_drvdata(pdev); + + if (di->chg_info.online_chg) + cancel_delayed_work_sync(&di->chargalg_wd_work); + + cancel_delayed_work_sync(&di->chargalg_periodic_work); + + return 0; +} +#else +#define abx500_chargalg_suspend NULL +#define abx500_chargalg_resume NULL +#endif + +static int abx500_chargalg_remove(struct platform_device *pdev) +{ + struct abx500_chargalg *di = platform_get_drvdata(pdev); + + /* sysfs interface to enable/disbale charging from user space */ + abx500_chargalg_sysfs_exit(di); + + hrtimer_cancel(&di->safety_timer); + hrtimer_cancel(&di->maintenance_timer); + + cancel_delayed_work_sync(&di->chargalg_periodic_work); + cancel_delayed_work_sync(&di->chargalg_wd_work); + cancel_work_sync(&di->chargalg_work); + + /* Delete the work queue */ + destroy_workqueue(di->chargalg_wq); + + power_supply_unregister(di->chargalg_psy); + + return 0; +} + +static char *supply_interface[] = { + "ab8500_fg", +}; + +static const struct power_supply_desc abx500_chargalg_desc = { + .name = "abx500_chargalg", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = abx500_chargalg_props, + .num_properties = ARRAY_SIZE(abx500_chargalg_props), + .get_property = abx500_chargalg_get_property, + .external_power_changed = abx500_chargalg_external_power_changed, +}; + +static int abx500_chargalg_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct abx500_bm_data *plat = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + struct abx500_chargalg *di; + int ret = 0; + + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); + if (!di) { + dev_err(&pdev->dev, "%s no mem for ab8500_chargalg\n", __func__); + return -ENOMEM; + } + + if (!plat) { + dev_err(&pdev->dev, "no battery management data supplied\n"); + return -EINVAL; + } + di->bm = plat; + + if (np) { + ret = ab8500_bm_of_probe(&pdev->dev, np, di->bm); + if (ret) { + dev_err(&pdev->dev, "failed to get battery information\n"); + return ret; + } + } + + /* get device struct and parent */ + di->dev = &pdev->dev; + di->parent = dev_get_drvdata(pdev->dev.parent); + + psy_cfg.supplied_to = supply_interface; + psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); + psy_cfg.drv_data = di; + + /* Initilialize safety timer */ + hrtimer_init(&di->safety_timer, CLOCK_REALTIME, HRTIMER_MODE_ABS); + di->safety_timer.function = abx500_chargalg_safety_timer_expired; + + /* Initilialize maintenance timer */ + hrtimer_init(&di->maintenance_timer, CLOCK_REALTIME, HRTIMER_MODE_ABS); + di->maintenance_timer.function = + abx500_chargalg_maintenance_timer_expired; + + /* Create a work queue for the chargalg */ + di->chargalg_wq = alloc_ordered_workqueue("abx500_chargalg_wq", + WQ_MEM_RECLAIM); + if (di->chargalg_wq == NULL) { + dev_err(di->dev, "failed to create work queue\n"); + return -ENOMEM; + } + + /* Init work for chargalg */ + INIT_DEFERRABLE_WORK(&di->chargalg_periodic_work, + abx500_chargalg_periodic_work); + INIT_DEFERRABLE_WORK(&di->chargalg_wd_work, + abx500_chargalg_wd_work); + + /* Init work for chargalg */ + INIT_WORK(&di->chargalg_work, abx500_chargalg_work); + + /* To detect charger at startup */ + di->chg_info.prev_conn_chg = -1; + + /* Register chargalg power supply class */ + di->chargalg_psy = power_supply_register(di->dev, &abx500_chargalg_desc, + &psy_cfg); + if (IS_ERR(di->chargalg_psy)) { + dev_err(di->dev, "failed to register chargalg psy\n"); + ret = PTR_ERR(di->chargalg_psy); + goto free_chargalg_wq; + } + + platform_set_drvdata(pdev, di); + + /* sysfs interface to enable/disable charging from user space */ + ret = abx500_chargalg_sysfs_init(di); + if (ret) { + dev_err(di->dev, "failed to create sysfs entry\n"); + goto free_psy; + } + di->curr_status.curr_step = CHARGALG_CURR_STEP_HIGH; + + /* Run the charging algorithm */ + queue_delayed_work(di->chargalg_wq, &di->chargalg_periodic_work, 0); + + dev_info(di->dev, "probe success\n"); + return ret; + +free_psy: + power_supply_unregister(di->chargalg_psy); +free_chargalg_wq: + destroy_workqueue(di->chargalg_wq); + return ret; +} + +static const struct of_device_id ab8500_chargalg_match[] = { + { .compatible = "stericsson,ab8500-chargalg", }, + { }, +}; + +static struct platform_driver abx500_chargalg_driver = { + .probe = abx500_chargalg_probe, + .remove = abx500_chargalg_remove, + .suspend = abx500_chargalg_suspend, + .resume = abx500_chargalg_resume, + .driver = { + .name = "ab8500-chargalg", + .of_match_table = ab8500_chargalg_match, + }, +}; + +module_platform_driver(abx500_chargalg_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Johan Palsson, Karl Komierowski"); +MODULE_ALIAS("platform:abx500-chargalg"); +MODULE_DESCRIPTION("abx500 battery charging algorithm"); diff --git a/drivers/power/supply/act8945a_charger.c b/drivers/power/supply/act8945a_charger.c new file mode 100644 index 000000000..5f3eb6941 --- /dev/null +++ b/drivers/power/supply/act8945a_charger.c @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Power supply driver for the Active-semi ACT8945A PMIC + * + * Copyright (C) 2015 Atmel Corporation + * + * Author: Wenyou Yang <wenyou.yang@atmel.com> + */ +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_irq.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/gpio/consumer.h> + +static const char *act8945a_charger_model = "ACT8945A"; +static const char *act8945a_charger_manufacturer = "Active-semi"; + +/** + * ACT8945A Charger Register Map + */ + +/* 0x70: Reserved */ +#define ACT8945A_APCH_CFG 0x71 +#define ACT8945A_APCH_STATUS 0x78 +#define ACT8945A_APCH_CTRL 0x79 +#define ACT8945A_APCH_STATE 0x7A + +/* ACT8945A_APCH_CFG */ +#define APCH_CFG_OVPSET (0x3 << 0) +#define APCH_CFG_OVPSET_6V6 (0x0 << 0) +#define APCH_CFG_OVPSET_7V (0x1 << 0) +#define APCH_CFG_OVPSET_7V5 (0x2 << 0) +#define APCH_CFG_OVPSET_8V (0x3 << 0) +#define APCH_CFG_PRETIMO (0x3 << 2) +#define APCH_CFG_PRETIMO_40_MIN (0x0 << 2) +#define APCH_CFG_PRETIMO_60_MIN (0x1 << 2) +#define APCH_CFG_PRETIMO_80_MIN (0x2 << 2) +#define APCH_CFG_PRETIMO_DISABLED (0x3 << 2) +#define APCH_CFG_TOTTIMO (0x3 << 4) +#define APCH_CFG_TOTTIMO_3_HOUR (0x0 << 4) +#define APCH_CFG_TOTTIMO_4_HOUR (0x1 << 4) +#define APCH_CFG_TOTTIMO_5_HOUR (0x2 << 4) +#define APCH_CFG_TOTTIMO_DISABLED (0x3 << 4) +#define APCH_CFG_SUSCHG (0x1 << 7) + +#define APCH_STATUS_CHGDAT BIT(0) +#define APCH_STATUS_INDAT BIT(1) +#define APCH_STATUS_TEMPDAT BIT(2) +#define APCH_STATUS_TIMRDAT BIT(3) +#define APCH_STATUS_CHGSTAT BIT(4) +#define APCH_STATUS_INSTAT BIT(5) +#define APCH_STATUS_TEMPSTAT BIT(6) +#define APCH_STATUS_TIMRSTAT BIT(7) + +#define APCH_CTRL_CHGEOCOUT BIT(0) +#define APCH_CTRL_INDIS BIT(1) +#define APCH_CTRL_TEMPOUT BIT(2) +#define APCH_CTRL_TIMRPRE BIT(3) +#define APCH_CTRL_CHGEOCIN BIT(4) +#define APCH_CTRL_INCON BIT(5) +#define APCH_CTRL_TEMPIN BIT(6) +#define APCH_CTRL_TIMRTOT BIT(7) + +#define APCH_STATE_ACINSTAT (0x1 << 1) +#define APCH_STATE_CSTATE (0x3 << 4) +#define APCH_STATE_CSTATE_SHIFT 4 +#define APCH_STATE_CSTATE_DISABLED 0x00 +#define APCH_STATE_CSTATE_EOC 0x01 +#define APCH_STATE_CSTATE_FAST 0x02 +#define APCH_STATE_CSTATE_PRE 0x03 + +struct act8945a_charger { + struct power_supply *psy; + struct power_supply_desc desc; + struct regmap *regmap; + struct work_struct work; + + bool init_done; + struct gpio_desc *lbo_gpio; + struct gpio_desc *chglev_gpio; +}; + +static int act8945a_get_charger_state(struct regmap *regmap, int *val) +{ + int ret; + unsigned int status, state; + + ret = regmap_read(regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + state &= APCH_STATE_CSTATE; + state >>= APCH_STATE_CSTATE_SHIFT; + + switch (state) { + case APCH_STATE_CSTATE_PRE: + case APCH_STATE_CSTATE_FAST: + *val = POWER_SUPPLY_STATUS_CHARGING; + break; + case APCH_STATE_CSTATE_EOC: + if (status & APCH_STATUS_CHGDAT) + *val = POWER_SUPPLY_STATUS_FULL; + else + *val = POWER_SUPPLY_STATUS_CHARGING; + break; + case APCH_STATE_CSTATE_DISABLED: + default: + if (!(status & APCH_STATUS_INDAT)) + *val = POWER_SUPPLY_STATUS_DISCHARGING; + else + *val = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + } + + return 0; +} + +static int act8945a_get_charge_type(struct regmap *regmap, int *val) +{ + int ret; + unsigned int status, state; + + ret = regmap_read(regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + state &= APCH_STATE_CSTATE; + state >>= APCH_STATE_CSTATE_SHIFT; + + switch (state) { + case APCH_STATE_CSTATE_PRE: + *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case APCH_STATE_CSTATE_FAST: + *val = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case APCH_STATE_CSTATE_EOC: + *val = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + case APCH_STATE_CSTATE_DISABLED: + default: + if (!(status & APCH_STATUS_INDAT)) + *val = POWER_SUPPLY_CHARGE_TYPE_NONE; + else + *val = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + break; + } + + return 0; +} + +static int act8945a_get_battery_health(struct regmap *regmap, int *val) +{ + int ret; + unsigned int status, state, config; + + ret = regmap_read(regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_CFG, &config); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + state &= APCH_STATE_CSTATE; + state >>= APCH_STATE_CSTATE_SHIFT; + + switch (state) { + case APCH_STATE_CSTATE_DISABLED: + if (config & APCH_CFG_SUSCHG) { + *val = POWER_SUPPLY_HEALTH_UNKNOWN; + } else if (status & APCH_STATUS_INDAT) { + if (!(status & APCH_STATUS_TEMPDAT)) + *val = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (status & APCH_STATUS_TIMRDAT) + *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + else + *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + } else { + *val = POWER_SUPPLY_HEALTH_GOOD; + } + break; + case APCH_STATE_CSTATE_PRE: + case APCH_STATE_CSTATE_FAST: + case APCH_STATE_CSTATE_EOC: + default: + *val = POWER_SUPPLY_HEALTH_GOOD; + break; + } + + return 0; +} + +static int act8945a_get_capacity_level(struct act8945a_charger *charger, + struct regmap *regmap, int *val) +{ + int ret; + unsigned int status, state, config; + int lbo_level = gpiod_get_value(charger->lbo_gpio); + + ret = regmap_read(regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_CFG, &config); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + state &= APCH_STATE_CSTATE; + state >>= APCH_STATE_CSTATE_SHIFT; + + switch (state) { + case APCH_STATE_CSTATE_PRE: + *val = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + break; + case APCH_STATE_CSTATE_FAST: + if (lbo_level) + *val = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else + *val = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + break; + case APCH_STATE_CSTATE_EOC: + if (status & APCH_STATUS_CHGDAT) + *val = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else + *val = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + break; + case APCH_STATE_CSTATE_DISABLED: + default: + if (config & APCH_CFG_SUSCHG) { + *val = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + } else { + *val = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + if (!(status & APCH_STATUS_INDAT)) { + if (!lbo_level) + *val = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + } + } + break; + } + + return 0; +} + +#define MAX_CURRENT_USB_HIGH 450000 +#define MAX_CURRENT_USB_LOW 90000 +#define MAX_CURRENT_USB_PRE 45000 +/* + * Riset(K) = 2336 * (1V/Ichg(mA)) - 0.205 + * Riset = 2.43K + */ +#define MAX_CURRENT_AC_HIGH 886527 +#define MAX_CURRENT_AC_LOW 117305 +#define MAX_CURRENT_AC_HIGH_PRE 88653 +#define MAX_CURRENT_AC_LOW_PRE 11731 + +static int act8945a_get_current_max(struct act8945a_charger *charger, + struct regmap *regmap, int *val) +{ + int ret; + unsigned int status, state; + unsigned int acin_state; + int chgin_level = gpiod_get_value(charger->chglev_gpio); + + ret = regmap_read(regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + acin_state = (state & APCH_STATE_ACINSTAT) >> 1; + + state &= APCH_STATE_CSTATE; + state >>= APCH_STATE_CSTATE_SHIFT; + + switch (state) { + case APCH_STATE_CSTATE_PRE: + if (acin_state) { + if (chgin_level) + *val = MAX_CURRENT_AC_HIGH_PRE; + else + *val = MAX_CURRENT_AC_LOW_PRE; + } else { + *val = MAX_CURRENT_USB_PRE; + } + break; + case APCH_STATE_CSTATE_FAST: + if (acin_state) { + if (chgin_level) + *val = MAX_CURRENT_AC_HIGH; + else + *val = MAX_CURRENT_AC_LOW; + } else { + if (chgin_level) + *val = MAX_CURRENT_USB_HIGH; + else + *val = MAX_CURRENT_USB_LOW; + } + break; + case APCH_STATE_CSTATE_EOC: + case APCH_STATE_CSTATE_DISABLED: + default: + *val = 0; + break; + } + + return 0; +} + +static enum power_supply_property act8945a_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER +}; + +static int act8945a_charger_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct act8945a_charger *charger = power_supply_get_drvdata(psy); + struct regmap *regmap = charger->regmap; + int ret = 0; + + switch (prop) { + case POWER_SUPPLY_PROP_STATUS: + ret = act8945a_get_charger_state(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = act8945a_get_charge_type(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = act8945a_get_battery_health(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + ret = act8945a_get_capacity_level(charger, + regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + ret = act8945a_get_current_max(charger, + regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = act8945a_charger_model; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = act8945a_charger_manufacturer; + break; + default: + return -EINVAL; + } + + return ret; +} + +static int act8945a_enable_interrupt(struct act8945a_charger *charger) +{ + struct regmap *regmap = charger->regmap; + unsigned char ctrl; + int ret; + + ctrl = APCH_CTRL_CHGEOCOUT | APCH_CTRL_CHGEOCIN | + APCH_CTRL_INDIS | APCH_CTRL_INCON | + APCH_CTRL_TEMPOUT | APCH_CTRL_TEMPIN | + APCH_CTRL_TIMRPRE | APCH_CTRL_TIMRTOT; + ret = regmap_write(regmap, ACT8945A_APCH_CTRL, ctrl); + if (ret) + return ret; + + ctrl = APCH_STATUS_CHGSTAT | APCH_STATUS_INSTAT | + APCH_STATUS_TEMPSTAT | APCH_STATUS_TIMRSTAT; + ret = regmap_write(regmap, ACT8945A_APCH_STATUS, ctrl); + if (ret) + return ret; + + return 0; +} + +static unsigned int act8945a_set_supply_type(struct act8945a_charger *charger, + unsigned int *type) +{ + unsigned int status, state; + int ret; + + ret = regmap_read(charger->regmap, ACT8945A_APCH_STATUS, &status); + if (ret < 0) + return ret; + + ret = regmap_read(charger->regmap, ACT8945A_APCH_STATE, &state); + if (ret < 0) + return ret; + + if (status & APCH_STATUS_INDAT) { + if (state & APCH_STATE_ACINSTAT) + *type = POWER_SUPPLY_TYPE_MAINS; + else + *type = POWER_SUPPLY_TYPE_USB; + } else { + *type = POWER_SUPPLY_TYPE_BATTERY; + } + + return 0; +} + +static void act8945a_work(struct work_struct *work) +{ + struct act8945a_charger *charger = + container_of(work, struct act8945a_charger, work); + + act8945a_set_supply_type(charger, &charger->desc.type); + + power_supply_changed(charger->psy); +} + +static irqreturn_t act8945a_status_changed(int irq, void *dev_id) +{ + struct act8945a_charger *charger = dev_id; + + if (charger->init_done) + schedule_work(&charger->work); + + return IRQ_HANDLED; +} + +#define DEFAULT_TOTAL_TIME_OUT 3 +#define DEFAULT_PRE_TIME_OUT 40 +#define DEFAULT_INPUT_OVP_THRESHOLD 6600 + +static int act8945a_charger_config(struct device *dev, + struct act8945a_charger *charger) +{ + struct device_node *np = dev->of_node; + struct regmap *regmap = charger->regmap; + + u32 total_time_out; + u32 pre_time_out; + u32 input_voltage_threshold; + int err, ret; + + unsigned int tmp; + unsigned int value = 0; + + if (!np) { + dev_err(dev, "no charger of node\n"); + return -EINVAL; + } + + ret = regmap_read(regmap, ACT8945A_APCH_CFG, &tmp); + if (ret) + return ret; + + if (tmp & APCH_CFG_SUSCHG) { + value |= APCH_CFG_SUSCHG; + dev_info(dev, "have been suspended\n"); + } + + charger->lbo_gpio = devm_gpiod_get_optional(dev, "active-semi,lbo", + GPIOD_IN); + if (IS_ERR(charger->lbo_gpio)) { + err = PTR_ERR(charger->lbo_gpio); + dev_err(dev, "unable to claim gpio \"lbo\": %d\n", err); + return err; + } + + ret = devm_request_irq(dev, gpiod_to_irq(charger->lbo_gpio), + act8945a_status_changed, + (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING), + "act8945a_lbo_detect", charger); + if (ret) + dev_info(dev, "failed to request gpio \"lbo\" IRQ\n"); + + charger->chglev_gpio = devm_gpiod_get_optional(dev, + "active-semi,chglev", + GPIOD_IN); + if (IS_ERR(charger->chglev_gpio)) { + err = PTR_ERR(charger->chglev_gpio); + dev_err(dev, "unable to claim gpio \"chglev\": %d\n", err); + return err; + } + + if (of_property_read_u32(np, + "active-semi,input-voltage-threshold-microvolt", + &input_voltage_threshold)) + input_voltage_threshold = DEFAULT_INPUT_OVP_THRESHOLD; + + if (of_property_read_u32(np, + "active-semi,precondition-timeout", + &pre_time_out)) + pre_time_out = DEFAULT_PRE_TIME_OUT; + + if (of_property_read_u32(np, "active-semi,total-timeout", + &total_time_out)) + total_time_out = DEFAULT_TOTAL_TIME_OUT; + + switch (input_voltage_threshold) { + case 8000: + value |= APCH_CFG_OVPSET_8V; + break; + case 7500: + value |= APCH_CFG_OVPSET_7V5; + break; + case 7000: + value |= APCH_CFG_OVPSET_7V; + break; + case 6600: + default: + value |= APCH_CFG_OVPSET_6V6; + break; + } + + switch (pre_time_out) { + case 60: + value |= APCH_CFG_PRETIMO_60_MIN; + break; + case 80: + value |= APCH_CFG_PRETIMO_80_MIN; + break; + case 0: + value |= APCH_CFG_PRETIMO_DISABLED; + break; + case 40: + default: + value |= APCH_CFG_PRETIMO_40_MIN; + break; + } + + switch (total_time_out) { + case 4: + value |= APCH_CFG_TOTTIMO_4_HOUR; + break; + case 5: + value |= APCH_CFG_TOTTIMO_5_HOUR; + break; + case 0: + value |= APCH_CFG_TOTTIMO_DISABLED; + break; + case 3: + default: + value |= APCH_CFG_TOTTIMO_3_HOUR; + break; + } + + return regmap_write(regmap, ACT8945A_APCH_CFG, value); +} + +static int act8945a_charger_probe(struct platform_device *pdev) +{ + struct act8945a_charger *charger; + struct power_supply_config psy_cfg = {}; + int irq, ret; + + charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + charger->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!charger->regmap) { + dev_err(&pdev->dev, "Parent did not provide regmap\n"); + return -EINVAL; + } + + ret = act8945a_charger_config(&pdev->dev, charger); + if (ret) + return ret; + + irq = of_irq_get(pdev->dev.of_node, 0); + if (irq <= 0) { + dev_err(&pdev->dev, "failed to find IRQ number\n"); + return irq ?: -ENXIO; + } + + ret = devm_request_irq(&pdev->dev, irq, act8945a_status_changed, + IRQF_TRIGGER_FALLING, "act8945a_interrupt", + charger); + if (ret) { + dev_err(&pdev->dev, "failed to request nIRQ pin IRQ\n"); + return ret; + } + + charger->desc.name = "act8945a-charger"; + charger->desc.get_property = act8945a_charger_get_property; + charger->desc.properties = act8945a_charger_props; + charger->desc.num_properties = ARRAY_SIZE(act8945a_charger_props); + + ret = act8945a_set_supply_type(charger, &charger->desc.type); + if (ret) + return -EINVAL; + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = charger; + + charger->psy = devm_power_supply_register(&pdev->dev, + &charger->desc, + &psy_cfg); + if (IS_ERR(charger->psy)) { + dev_err(&pdev->dev, "failed to register power supply\n"); + return PTR_ERR(charger->psy); + } + + platform_set_drvdata(pdev, charger); + + INIT_WORK(&charger->work, act8945a_work); + + ret = act8945a_enable_interrupt(charger); + if (ret) + return -EIO; + + charger->init_done = true; + + return 0; +} + +static int act8945a_charger_remove(struct platform_device *pdev) +{ + struct act8945a_charger *charger = platform_get_drvdata(pdev); + + charger->init_done = false; + cancel_work_sync(&charger->work); + + return 0; +} + +static struct platform_driver act8945a_charger_driver = { + .driver = { + .name = "act8945a-charger", + }, + .probe = act8945a_charger_probe, + .remove = act8945a_charger_remove, +}; +module_platform_driver(act8945a_charger_driver); + +MODULE_DESCRIPTION("Active-semi ACT8945A ActivePath charger driver"); +MODULE_AUTHOR("Wenyou Yang <wenyou.yang@atmel.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/adp5061.c b/drivers/power/supply/adp5061.c new file mode 100644 index 000000000..daee1161c --- /dev/null +++ b/drivers/power/supply/adp5061.c @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ADP5061 I2C Programmable Linear Battery Charger + * + * Copyright 2018 Analog Devices Inc. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/mod_devicetable.h> +#include <linux/power_supply.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/regmap.h> + +/* ADP5061 registers definition */ +#define ADP5061_ID 0x00 +#define ADP5061_REV 0x01 +#define ADP5061_VINX_SET 0x02 +#define ADP5061_TERM_SET 0x03 +#define ADP5061_CHG_CURR 0x04 +#define ADP5061_VOLTAGE_TH 0x05 +#define ADP5061_TIMER_SET 0x06 +#define ADP5061_FUNC_SET_1 0x07 +#define ADP5061_FUNC_SET_2 0x08 +#define ADP5061_INT_EN 0x09 +#define ADP5061_INT_ACT 0x0A +#define ADP5061_CHG_STATUS_1 0x0B +#define ADP5061_CHG_STATUS_2 0x0C +#define ADP5061_FAULT 0x0D +#define ADP5061_BATTERY_SHORT 0x10 +#define ADP5061_IEND 0x11 + +/* ADP5061_VINX_SET */ +#define ADP5061_VINX_SET_ILIM_MSK GENMASK(3, 0) +#define ADP5061_VINX_SET_ILIM_MODE(x) (((x) & 0x0F) << 0) + +/* ADP5061_TERM_SET */ +#define ADP5061_TERM_SET_VTRM_MSK GENMASK(7, 2) +#define ADP5061_TERM_SET_VTRM_MODE(x) (((x) & 0x3F) << 2) +#define ADP5061_TERM_SET_CHG_VLIM_MSK GENMASK(1, 0) +#define ADP5061_TERM_SET_CHG_VLIM_MODE(x) (((x) & 0x03) << 0) + +/* ADP5061_CHG_CURR */ +#define ADP5061_CHG_CURR_ICHG_MSK GENMASK(6, 2) +#define ADP5061_CHG_CURR_ICHG_MODE(x) (((x) & 0x1F) << 2) +#define ADP5061_CHG_CURR_ITRK_DEAD_MSK GENMASK(1, 0) +#define ADP5061_CHG_CURR_ITRK_DEAD_MODE(x) (((x) & 0x03) << 0) + +/* ADP5061_VOLTAGE_TH */ +#define ADP5061_VOLTAGE_TH_DIS_RCH_MSK BIT(7) +#define ADP5061_VOLTAGE_TH_DIS_RCH_MODE(x) (((x) & 0x01) << 7) +#define ADP5061_VOLTAGE_TH_VRCH_MSK GENMASK(6, 5) +#define ADP5061_VOLTAGE_TH_VRCH_MODE(x) (((x) & 0x03) << 5) +#define ADP5061_VOLTAGE_TH_VTRK_DEAD_MSK GENMASK(4, 3) +#define ADP5061_VOLTAGE_TH_VTRK_DEAD_MODE(x) (((x) & 0x03) << 3) +#define ADP5061_VOLTAGE_TH_VWEAK_MSK GENMASK(2, 0) +#define ADP5061_VOLTAGE_TH_VWEAK_MODE(x) (((x) & 0x07) << 0) + +/* ADP5061_CHG_STATUS_1 */ +#define ADP5061_CHG_STATUS_1_VIN_OV(x) (((x) >> 7) & 0x1) +#define ADP5061_CHG_STATUS_1_VIN_OK(x) (((x) >> 6) & 0x1) +#define ADP5061_CHG_STATUS_1_VIN_ILIM(x) (((x) >> 5) & 0x1) +#define ADP5061_CHG_STATUS_1_THERM_LIM(x) (((x) >> 4) & 0x1) +#define ADP5061_CHG_STATUS_1_CHDONE(x) (((x) >> 3) & 0x1) +#define ADP5061_CHG_STATUS_1_CHG_STATUS(x) (((x) >> 0) & 0x7) + +/* ADP5061_CHG_STATUS_2 */ +#define ADP5061_CHG_STATUS_2_THR_STATUS(x) (((x) >> 5) & 0x7) +#define ADP5061_CHG_STATUS_2_RCH_LIM_INFO(x) (((x) >> 3) & 0x1) +#define ADP5061_CHG_STATUS_2_BAT_STATUS(x) (((x) >> 0) & 0x7) + +/* ADP5061_IEND */ +#define ADP5061_IEND_IEND_MSK GENMASK(7, 5) +#define ADP5061_IEND_IEND_MODE(x) (((x) & 0x07) << 5) + +#define ADP5061_NO_BATTERY 0x01 +#define ADP5061_ICHG_MAX 1300 // mA + +enum adp5061_chg_status { + ADP5061_CHG_OFF, + ADP5061_CHG_TRICKLE, + ADP5061_CHG_FAST_CC, + ADP5061_CHG_FAST_CV, + ADP5061_CHG_COMPLETE, + ADP5061_CHG_LDO_MODE, + ADP5061_CHG_TIMER_EXP, + ADP5061_CHG_BAT_DET, +}; + +static const int adp5061_chg_type[4] = { + [ADP5061_CHG_OFF] = POWER_SUPPLY_CHARGE_TYPE_NONE, + [ADP5061_CHG_TRICKLE] = POWER_SUPPLY_CHARGE_TYPE_TRICKLE, + [ADP5061_CHG_FAST_CC] = POWER_SUPPLY_CHARGE_TYPE_FAST, + [ADP5061_CHG_FAST_CV] = POWER_SUPPLY_CHARGE_TYPE_FAST, +}; + +static const int adp5061_vweak_th[8] = { + 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, +}; + +static const int adp5061_prechg_current[4] = { + 5, 10, 20, 80, +}; + +static const int adp5061_vmin[4] = { + 2000, 2500, 2600, 2900, +}; + +static const int adp5061_const_chg_vmax[4] = { + 3200, 3400, 3700, 3800, +}; + +static const int adp5061_const_ichg[24] = { + 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, + 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1200, 1300, +}; + +static const int adp5061_vmax[36] = { + 3800, 3820, 3840, 3860, 3880, 3900, 3920, 3940, 3960, 3980, + 4000, 4020, 4040, 4060, 4080, 4100, 4120, 4140, 4160, 4180, + 4200, 4220, 4240, 4260, 4280, 4300, 4320, 4340, 4360, 4380, + 4400, 4420, 4440, 4460, 4480, 4500, +}; + +static const int adp5061_in_current_lim[16] = { + 100, 150, 200, 250, 300, 400, 500, 600, 700, + 800, 900, 1000, 1200, 1500, 1800, 2100, +}; + +static const int adp5061_iend[8] = { + 12500, 32500, 52500, 72500, 92500, 117500, 142500, 170000, +}; + +struct adp5061_state { + struct i2c_client *client; + struct regmap *regmap; + struct power_supply *psy; +}; + +static int adp5061_get_array_index(const int *array, u8 size, int val) +{ + int i; + + for (i = 1; i < size; i++) { + if (val < array[i]) + break; + } + + return i-1; +} + +static int adp5061_get_status(struct adp5061_state *st, + u8 *status1, u8 *status2) +{ + u8 buf[2]; + int ret; + + /* CHG_STATUS1 and CHG_STATUS2 are adjacent regs */ + ret = regmap_bulk_read(st->regmap, ADP5061_CHG_STATUS_1, + &buf[0], 2); + if (ret < 0) + return ret; + + *status1 = buf[0]; + *status2 = buf[1]; + + return ret; +} + +static int adp5061_get_input_current_limit(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int mode, ret; + + ret = regmap_read(st->regmap, ADP5061_VINX_SET, ®val); + if (ret < 0) + return ret; + + mode = ADP5061_VINX_SET_ILIM_MODE(regval); + val->intval = adp5061_in_current_lim[mode] * 1000; + + return ret; +} + +static int adp5061_set_input_current_limit(struct adp5061_state *st, int val) +{ + int index; + + /* Convert from uA to mA */ + val /= 1000; + index = adp5061_get_array_index(adp5061_in_current_lim, + ARRAY_SIZE(adp5061_in_current_lim), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_VINX_SET, + ADP5061_VINX_SET_ILIM_MSK, + ADP5061_VINX_SET_ILIM_MODE(index)); +} + +static int adp5061_set_min_voltage(struct adp5061_state *st, int val) +{ + int index; + + /* Convert from uV to mV */ + val /= 1000; + index = adp5061_get_array_index(adp5061_vmin, + ARRAY_SIZE(adp5061_vmin), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_VOLTAGE_TH, + ADP5061_VOLTAGE_TH_VTRK_DEAD_MSK, + ADP5061_VOLTAGE_TH_VTRK_DEAD_MODE(index)); +} + +static int adp5061_get_min_voltage(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_VOLTAGE_TH, ®val); + if (ret < 0) + return ret; + + regval = ((regval & ADP5061_VOLTAGE_TH_VTRK_DEAD_MSK) >> 3); + val->intval = adp5061_vmin[regval] * 1000; + + return ret; +} + +static int adp5061_get_chg_volt_lim(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int mode, ret; + + ret = regmap_read(st->regmap, ADP5061_TERM_SET, ®val); + if (ret < 0) + return ret; + + mode = ADP5061_TERM_SET_CHG_VLIM_MODE(regval); + val->intval = adp5061_const_chg_vmax[mode] * 1000; + + return ret; +} + +static int adp5061_get_max_voltage(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_TERM_SET, ®val); + if (ret < 0) + return ret; + + regval = ((regval & ADP5061_TERM_SET_VTRM_MSK) >> 2) - 0x0F; + if (regval >= ARRAY_SIZE(adp5061_vmax)) + regval = ARRAY_SIZE(adp5061_vmax) - 1; + + val->intval = adp5061_vmax[regval] * 1000; + + return ret; +} + +static int adp5061_set_max_voltage(struct adp5061_state *st, int val) +{ + int vmax_index; + + /* Convert from uV to mV */ + val /= 1000; + if (val > 4500) + val = 4500; + + vmax_index = adp5061_get_array_index(adp5061_vmax, + ARRAY_SIZE(adp5061_vmax), val); + if (vmax_index < 0) + return vmax_index; + + vmax_index += 0x0F; + + return regmap_update_bits(st->regmap, ADP5061_TERM_SET, + ADP5061_TERM_SET_VTRM_MSK, + ADP5061_TERM_SET_VTRM_MODE(vmax_index)); +} + +static int adp5061_set_const_chg_vmax(struct adp5061_state *st, int val) +{ + int index; + + /* Convert from uV to mV */ + val /= 1000; + index = adp5061_get_array_index(adp5061_const_chg_vmax, + ARRAY_SIZE(adp5061_const_chg_vmax), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_TERM_SET, + ADP5061_TERM_SET_CHG_VLIM_MSK, + ADP5061_TERM_SET_CHG_VLIM_MODE(index)); +} + +static int adp5061_set_const_chg_current(struct adp5061_state *st, int val) +{ + + int index; + + /* Convert from uA to mA */ + val /= 1000; + if (val > ADP5061_ICHG_MAX) + val = ADP5061_ICHG_MAX; + + index = adp5061_get_array_index(adp5061_const_ichg, + ARRAY_SIZE(adp5061_const_ichg), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_CHG_CURR, + ADP5061_CHG_CURR_ICHG_MSK, + ADP5061_CHG_CURR_ICHG_MODE(index)); +} + +static int adp5061_get_const_chg_current(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_CHG_CURR, ®val); + if (ret < 0) + return ret; + + regval = ((regval & ADP5061_CHG_CURR_ICHG_MSK) >> 2); + if (regval >= ARRAY_SIZE(adp5061_const_ichg)) + regval = ARRAY_SIZE(adp5061_const_ichg) - 1; + + val->intval = adp5061_const_ichg[regval] * 1000; + + return ret; +} + +static int adp5061_get_prechg_current(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_CHG_CURR, ®val); + if (ret < 0) + return ret; + + regval &= ADP5061_CHG_CURR_ITRK_DEAD_MSK; + val->intval = adp5061_prechg_current[regval] * 1000; + + return ret; +} + +static int adp5061_set_prechg_current(struct adp5061_state *st, int val) +{ + int index; + + /* Convert from uA to mA */ + val /= 1000; + index = adp5061_get_array_index(adp5061_prechg_current, + ARRAY_SIZE(adp5061_prechg_current), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_CHG_CURR, + ADP5061_CHG_CURR_ITRK_DEAD_MSK, + ADP5061_CHG_CURR_ITRK_DEAD_MODE(index)); +} + +static int adp5061_get_vweak_th(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_VOLTAGE_TH, ®val); + if (ret < 0) + return ret; + + regval &= ADP5061_VOLTAGE_TH_VWEAK_MSK; + val->intval = adp5061_vweak_th[regval] * 1000; + + return ret; +} + +static int adp5061_set_vweak_th(struct adp5061_state *st, int val) +{ + int index; + + /* Convert from uV to mV */ + val /= 1000; + index = adp5061_get_array_index(adp5061_vweak_th, + ARRAY_SIZE(adp5061_vweak_th), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_VOLTAGE_TH, + ADP5061_VOLTAGE_TH_VWEAK_MSK, + ADP5061_VOLTAGE_TH_VWEAK_MODE(index)); +} + +static int adp5061_get_chg_type(struct adp5061_state *st, + union power_supply_propval *val) +{ + u8 status1, status2; + int chg_type, ret; + + ret = adp5061_get_status(st, &status1, &status2); + if (ret < 0) + return ret; + + chg_type = ADP5061_CHG_STATUS_1_CHG_STATUS(status1); + if (chg_type >= ARRAY_SIZE(adp5061_chg_type)) + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + else + val->intval = adp5061_chg_type[chg_type]; + + return ret; +} + +static int adp5061_get_charger_status(struct adp5061_state *st, + union power_supply_propval *val) +{ + u8 status1, status2; + int ret; + + ret = adp5061_get_status(st, &status1, &status2); + if (ret < 0) + return ret; + + switch (ADP5061_CHG_STATUS_1_CHG_STATUS(status1)) { + case ADP5061_CHG_OFF: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case ADP5061_CHG_TRICKLE: + case ADP5061_CHG_FAST_CC: + case ADP5061_CHG_FAST_CV: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case ADP5061_CHG_COMPLETE: + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + case ADP5061_CHG_TIMER_EXP: + /* The battery must be discharging if there is a charge fault */ + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + } + + return ret; +} + +static int adp5061_get_battery_status(struct adp5061_state *st, + union power_supply_propval *val) +{ + u8 status1, status2; + int ret; + + ret = adp5061_get_status(st, &status1, &status2); + if (ret < 0) + return ret; + + switch (ADP5061_CHG_STATUS_2_BAT_STATUS(status2)) { + case 0x0: /* Battery monitor off */ + case 0x1: /* No battery */ + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + break; + case 0x2: /* VBAT < VTRK */ + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + break; + case 0x3: /* VTRK < VBAT_SNS < VWEAK */ + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + break; + case 0x4: /* VBAT_SNS > VWEAK */ + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + break; + } + + return ret; +} + +static int adp5061_get_termination_current(struct adp5061_state *st, + union power_supply_propval *val) +{ + unsigned int regval; + int ret; + + ret = regmap_read(st->regmap, ADP5061_IEND, ®val); + if (ret < 0) + return ret; + + regval = (regval & ADP5061_IEND_IEND_MSK) >> 5; + val->intval = adp5061_iend[regval]; + + return ret; +} + +static int adp5061_set_termination_current(struct adp5061_state *st, int val) +{ + int index; + + index = adp5061_get_array_index(adp5061_iend, + ARRAY_SIZE(adp5061_iend), + val); + if (index < 0) + return index; + + return regmap_update_bits(st->regmap, ADP5061_IEND, + ADP5061_IEND_IEND_MSK, + ADP5061_IEND_IEND_MODE(index)); +} + +static int adp5061_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct adp5061_state *st = power_supply_get_drvdata(psy); + u8 status1, status2; + int mode, ret; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + ret = adp5061_get_status(st, &status1, &status2); + if (ret < 0) + return ret; + + mode = ADP5061_CHG_STATUS_2_BAT_STATUS(status2); + if (mode == ADP5061_NO_BATTERY) + val->intval = 0; + else + val->intval = 1; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + return adp5061_get_chg_type(st, val); + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + /* This property is used to indicate the input current + * limit into VINx (ILIM) + */ + return adp5061_get_input_current_limit(st, val); + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + /* This property is used to indicate the termination + * voltage (VTRM) + */ + return adp5061_get_max_voltage(st, val); + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + /* + * This property is used to indicate the trickle to fast + * charge threshold (VTRK_DEAD) + */ + return adp5061_get_min_voltage(st, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + /* This property is used to indicate the charging + * voltage limit (CHG_VLIM) + */ + return adp5061_get_chg_volt_lim(st, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + /* + * This property is used to indicate the value of the constant + * current charge (ICHG) + */ + return adp5061_get_const_chg_current(st, val); + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + /* + * This property is used to indicate the value of the trickle + * and weak charge currents (ITRK_DEAD) + */ + return adp5061_get_prechg_current(st, val); + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + /* + * This property is used to set the VWEAK threshold + * bellow this value, weak charge mode is entered + * above this value, fast chargerge mode is entered + */ + return adp5061_get_vweak_th(st, val); + case POWER_SUPPLY_PROP_STATUS: + /* + * Indicate the charger status in relation to power + * supply status property + */ + return adp5061_get_charger_status(st, val); + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + /* + * Indicate the battery status in relation to power + * supply capacity level property + */ + return adp5061_get_battery_status(st, val); + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + /* Indicate the values of the termination current */ + return adp5061_get_termination_current(st, val); + default: + return -EINVAL; + } + + return 0; +} + +static int adp5061_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct adp5061_state *st = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return adp5061_set_input_current_limit(st, val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + return adp5061_set_max_voltage(st, val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + return adp5061_set_min_voltage(st, val->intval); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + return adp5061_set_const_chg_vmax(st, val->intval); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return adp5061_set_const_chg_current(st, val->intval); + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + return adp5061_set_prechg_current(st, val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + return adp5061_set_vweak_th(st, val->intval); + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + return adp5061_set_termination_current(st, val->intval); + default: + return -EINVAL; + } + + return 0; +} + +static int adp5061_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + return 1; + default: + return 0; + } +} + +static enum power_supply_property adp5061_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_PRECHARGE_CURRENT, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, +}; + +static const struct regmap_config adp5061_regmap_config = { + .reg_bits = 8, + .val_bits = 8, +}; + +static const struct power_supply_desc adp5061_desc = { + .name = "adp5061", + .type = POWER_SUPPLY_TYPE_USB, + .get_property = adp5061_get_property, + .set_property = adp5061_set_property, + .property_is_writeable = adp5061_prop_writeable, + .properties = adp5061_props, + .num_properties = ARRAY_SIZE(adp5061_props), +}; + +static int adp5061_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct power_supply_config psy_cfg = {}; + struct adp5061_state *st; + + st = devm_kzalloc(&client->dev, sizeof(*st), GFP_KERNEL); + if (!st) + return -ENOMEM; + + st->client = client; + st->regmap = devm_regmap_init_i2c(client, + &adp5061_regmap_config); + if (IS_ERR(st->regmap)) { + dev_err(&client->dev, "Failed to initialize register map\n"); + return -EINVAL; + } + + i2c_set_clientdata(client, st); + psy_cfg.drv_data = st; + + st->psy = devm_power_supply_register(&client->dev, + &adp5061_desc, + &psy_cfg); + + if (IS_ERR(st->psy)) { + dev_err(&client->dev, "Failed to register power supply\n"); + return PTR_ERR(st->psy); + } + + return 0; +} + +static const struct i2c_device_id adp5061_id[] = { + { "adp5061", 0}, + { } +}; +MODULE_DEVICE_TABLE(i2c, adp5061_id); + +static struct i2c_driver adp5061_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, + .probe = adp5061_probe, + .id_table = adp5061_id, +}; +module_i2c_driver(adp5061_driver); + +MODULE_DESCRIPTION("Analog Devices adp5061 battery charger driver"); +MODULE_AUTHOR("Stefan Popa <stefan.popa@analog.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/apm_power.c b/drivers/power/supply/apm_power.c new file mode 100644 index 000000000..9d1a7fbca --- /dev/null +++ b/drivers/power/supply/apm_power.c @@ -0,0 +1,376 @@ +/* + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + * Copyright © 2007 Eugeny Boger <eugenyboger@dgap.mipt.ru> + * + * Author: Eugeny Boger <eugenyboger@dgap.mipt.ru> + * + * Use consistent with the GNU GPL is permitted, + * provided that this copyright notice is + * preserved in its entirety in all copies and derived works. + */ + +#include <linux/module.h> +#include <linux/device.h> +#include <linux/power_supply.h> +#include <linux/apm-emulation.h> + + +#define PSY_PROP(psy, prop, val) (power_supply_get_property(psy, \ + POWER_SUPPLY_PROP_##prop, val)) + +#define _MPSY_PROP(prop, val) (power_supply_get_property(main_battery, \ + prop, val)) + +#define MPSY_PROP(prop, val) _MPSY_PROP(POWER_SUPPLY_PROP_##prop, val) + +static DEFINE_MUTEX(apm_mutex); +static struct power_supply *main_battery; + +enum apm_source { + SOURCE_ENERGY, + SOURCE_CHARGE, + SOURCE_VOLTAGE, +}; + +struct find_bat_param { + struct power_supply *main; + struct power_supply *bat; + struct power_supply *max_charge_bat; + struct power_supply *max_energy_bat; + union power_supply_propval full; + int max_charge; + int max_energy; +}; + +static int __find_main_battery(struct device *dev, void *data) +{ + struct find_bat_param *bp = (struct find_bat_param *)data; + + bp->bat = dev_get_drvdata(dev); + + if (bp->bat->desc->use_for_apm) { + /* nice, we explicitly asked to report this battery. */ + bp->main = bp->bat; + return 1; + } + + if (!PSY_PROP(bp->bat, CHARGE_FULL_DESIGN, &bp->full) || + !PSY_PROP(bp->bat, CHARGE_FULL, &bp->full)) { + if (bp->full.intval > bp->max_charge) { + bp->max_charge_bat = bp->bat; + bp->max_charge = bp->full.intval; + } + } else if (!PSY_PROP(bp->bat, ENERGY_FULL_DESIGN, &bp->full) || + !PSY_PROP(bp->bat, ENERGY_FULL, &bp->full)) { + if (bp->full.intval > bp->max_energy) { + bp->max_energy_bat = bp->bat; + bp->max_energy = bp->full.intval; + } + } + return 0; +} + +static void find_main_battery(void) +{ + struct find_bat_param bp; + int error; + + memset(&bp, 0, sizeof(struct find_bat_param)); + main_battery = NULL; + bp.main = main_battery; + + error = class_for_each_device(power_supply_class, NULL, &bp, + __find_main_battery); + if (error) { + main_battery = bp.main; + return; + } + + if ((bp.max_energy_bat && bp.max_charge_bat) && + (bp.max_energy_bat != bp.max_charge_bat)) { + /* try guess battery with more capacity */ + if (!PSY_PROP(bp.max_charge_bat, VOLTAGE_MAX_DESIGN, + &bp.full)) { + if (bp.max_energy > bp.max_charge * bp.full.intval) + main_battery = bp.max_energy_bat; + else + main_battery = bp.max_charge_bat; + } else if (!PSY_PROP(bp.max_energy_bat, VOLTAGE_MAX_DESIGN, + &bp.full)) { + if (bp.max_charge > bp.max_energy / bp.full.intval) + main_battery = bp.max_charge_bat; + else + main_battery = bp.max_energy_bat; + } else { + /* give up, choice any */ + main_battery = bp.max_energy_bat; + } + } else if (bp.max_charge_bat) { + main_battery = bp.max_charge_bat; + } else if (bp.max_energy_bat) { + main_battery = bp.max_energy_bat; + } else { + /* give up, try the last if any */ + main_battery = bp.bat; + } +} + +static int do_calculate_time(int status, enum apm_source source) +{ + union power_supply_propval full; + union power_supply_propval empty; + union power_supply_propval cur; + union power_supply_propval I; + enum power_supply_property full_prop; + enum power_supply_property full_design_prop; + enum power_supply_property empty_prop; + enum power_supply_property empty_design_prop; + enum power_supply_property cur_avg_prop; + enum power_supply_property cur_now_prop; + + if (MPSY_PROP(CURRENT_AVG, &I)) { + /* if battery can't report average value, use momentary */ + if (MPSY_PROP(CURRENT_NOW, &I)) + return -1; + } + + if (!I.intval) + return 0; + + switch (source) { + case SOURCE_CHARGE: + full_prop = POWER_SUPPLY_PROP_CHARGE_FULL; + full_design_prop = POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN; + empty_prop = POWER_SUPPLY_PROP_CHARGE_EMPTY; + empty_design_prop = POWER_SUPPLY_PROP_CHARGE_EMPTY; + cur_avg_prop = POWER_SUPPLY_PROP_CHARGE_AVG; + cur_now_prop = POWER_SUPPLY_PROP_CHARGE_NOW; + break; + case SOURCE_ENERGY: + full_prop = POWER_SUPPLY_PROP_ENERGY_FULL; + full_design_prop = POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN; + empty_prop = POWER_SUPPLY_PROP_ENERGY_EMPTY; + empty_design_prop = POWER_SUPPLY_PROP_CHARGE_EMPTY; + cur_avg_prop = POWER_SUPPLY_PROP_ENERGY_AVG; + cur_now_prop = POWER_SUPPLY_PROP_ENERGY_NOW; + break; + case SOURCE_VOLTAGE: + full_prop = POWER_SUPPLY_PROP_VOLTAGE_MAX; + full_design_prop = POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN; + empty_prop = POWER_SUPPLY_PROP_VOLTAGE_MIN; + empty_design_prop = POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN; + cur_avg_prop = POWER_SUPPLY_PROP_VOLTAGE_AVG; + cur_now_prop = POWER_SUPPLY_PROP_VOLTAGE_NOW; + break; + default: + printk(KERN_ERR "Unsupported source: %d\n", source); + return -1; + } + + if (_MPSY_PROP(full_prop, &full)) { + /* if battery can't report this property, use design value */ + if (_MPSY_PROP(full_design_prop, &full)) + return -1; + } + + if (_MPSY_PROP(empty_prop, &empty)) { + /* if battery can't report this property, use design value */ + if (_MPSY_PROP(empty_design_prop, &empty)) + empty.intval = 0; + } + + if (_MPSY_PROP(cur_avg_prop, &cur)) { + /* if battery can't report average value, use momentary */ + if (_MPSY_PROP(cur_now_prop, &cur)) + return -1; + } + + if (status == POWER_SUPPLY_STATUS_CHARGING) + return ((cur.intval - full.intval) * 60L) / I.intval; + else + return -((cur.intval - empty.intval) * 60L) / I.intval; +} + +static int calculate_time(int status) +{ + int time; + + time = do_calculate_time(status, SOURCE_ENERGY); + if (time != -1) + return time; + + time = do_calculate_time(status, SOURCE_CHARGE); + if (time != -1) + return time; + + time = do_calculate_time(status, SOURCE_VOLTAGE); + if (time != -1) + return time; + + return -1; +} + +static int calculate_capacity(enum apm_source source) +{ + enum power_supply_property full_prop, empty_prop; + enum power_supply_property full_design_prop, empty_design_prop; + enum power_supply_property now_prop, avg_prop; + union power_supply_propval empty, full, cur; + int ret; + + switch (source) { + case SOURCE_CHARGE: + full_prop = POWER_SUPPLY_PROP_CHARGE_FULL; + empty_prop = POWER_SUPPLY_PROP_CHARGE_EMPTY; + full_design_prop = POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN; + empty_design_prop = POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN; + now_prop = POWER_SUPPLY_PROP_CHARGE_NOW; + avg_prop = POWER_SUPPLY_PROP_CHARGE_AVG; + break; + case SOURCE_ENERGY: + full_prop = POWER_SUPPLY_PROP_ENERGY_FULL; + empty_prop = POWER_SUPPLY_PROP_ENERGY_EMPTY; + full_design_prop = POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN; + empty_design_prop = POWER_SUPPLY_PROP_ENERGY_EMPTY_DESIGN; + now_prop = POWER_SUPPLY_PROP_ENERGY_NOW; + avg_prop = POWER_SUPPLY_PROP_ENERGY_AVG; + break; + case SOURCE_VOLTAGE: + full_prop = POWER_SUPPLY_PROP_VOLTAGE_MAX; + empty_prop = POWER_SUPPLY_PROP_VOLTAGE_MIN; + full_design_prop = POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN; + empty_design_prop = POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN; + now_prop = POWER_SUPPLY_PROP_VOLTAGE_NOW; + avg_prop = POWER_SUPPLY_PROP_VOLTAGE_AVG; + break; + default: + printk(KERN_ERR "Unsupported source: %d\n", source); + return -1; + } + + if (_MPSY_PROP(full_prop, &full)) { + /* if battery can't report this property, use design value */ + if (_MPSY_PROP(full_design_prop, &full)) + return -1; + } + + if (_MPSY_PROP(avg_prop, &cur)) { + /* if battery can't report average value, use momentary */ + if (_MPSY_PROP(now_prop, &cur)) + return -1; + } + + if (_MPSY_PROP(empty_prop, &empty)) { + /* if battery can't report this property, use design value */ + if (_MPSY_PROP(empty_design_prop, &empty)) + empty.intval = 0; + } + + if (full.intval - empty.intval) + ret = ((cur.intval - empty.intval) * 100L) / + (full.intval - empty.intval); + else + return -1; + + if (ret > 100) + return 100; + else if (ret < 0) + return 0; + + return ret; +} + +static void apm_battery_apm_get_power_status(struct apm_power_info *info) +{ + union power_supply_propval status; + union power_supply_propval capacity, time_to_full, time_to_empty; + + mutex_lock(&apm_mutex); + find_main_battery(); + if (!main_battery) { + mutex_unlock(&apm_mutex); + return; + } + + /* status */ + + if (MPSY_PROP(STATUS, &status)) + status.intval = POWER_SUPPLY_STATUS_UNKNOWN; + + /* ac line status */ + + if ((status.intval == POWER_SUPPLY_STATUS_CHARGING) || + (status.intval == POWER_SUPPLY_STATUS_NOT_CHARGING) || + (status.intval == POWER_SUPPLY_STATUS_FULL)) + info->ac_line_status = APM_AC_ONLINE; + else + info->ac_line_status = APM_AC_OFFLINE; + + /* battery life (i.e. capacity, in percents) */ + + if (MPSY_PROP(CAPACITY, &capacity) == 0) { + info->battery_life = capacity.intval; + } else { + /* try calculate using energy */ + info->battery_life = calculate_capacity(SOURCE_ENERGY); + /* if failed try calculate using charge instead */ + if (info->battery_life == -1) + info->battery_life = calculate_capacity(SOURCE_CHARGE); + if (info->battery_life == -1) + info->battery_life = calculate_capacity(SOURCE_VOLTAGE); + } + + /* charging status */ + + if (status.intval == POWER_SUPPLY_STATUS_CHARGING) { + info->battery_status = APM_BATTERY_STATUS_CHARGING; + } else { + if (info->battery_life > 50) + info->battery_status = APM_BATTERY_STATUS_HIGH; + else if (info->battery_life > 5) + info->battery_status = APM_BATTERY_STATUS_LOW; + else + info->battery_status = APM_BATTERY_STATUS_CRITICAL; + } + info->battery_flag = info->battery_status; + + /* time */ + + info->units = APM_UNITS_MINS; + + if (status.intval == POWER_SUPPLY_STATUS_CHARGING) { + if (!MPSY_PROP(TIME_TO_FULL_AVG, &time_to_full) || + !MPSY_PROP(TIME_TO_FULL_NOW, &time_to_full)) + info->time = time_to_full.intval / 60; + else + info->time = calculate_time(status.intval); + } else { + if (!MPSY_PROP(TIME_TO_EMPTY_AVG, &time_to_empty) || + !MPSY_PROP(TIME_TO_EMPTY_NOW, &time_to_empty)) + info->time = time_to_empty.intval / 60; + else + info->time = calculate_time(status.intval); + } + + mutex_unlock(&apm_mutex); +} + +static int __init apm_battery_init(void) +{ + printk(KERN_INFO "APM Battery Driver\n"); + + apm_get_power_status = apm_battery_apm_get_power_status; + return 0; +} + +static void __exit apm_battery_exit(void) +{ + apm_get_power_status = NULL; +} + +module_init(apm_battery_init); +module_exit(apm_battery_exit); + +MODULE_AUTHOR("Eugeny Boger <eugenyboger@dgap.mipt.ru>"); +MODULE_DESCRIPTION("APM emulation driver for battery monitoring class"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/axp20x_ac_power.c b/drivers/power/supply/axp20x_ac_power.c new file mode 100644 index 000000000..ac360016b --- /dev/null +++ b/drivers/power/supply/axp20x_ac_power.c @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AXP20X and AXP22X PMICs' ACIN power supply driver + * + * Copyright (C) 2016 Free Electrons + * Quentin Schulz <quentin.schulz@free-electrons.com> + */ + +#include <linux/device.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/mfd/axp20x.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/iio/consumer.h> + +#define AXP20X_PWR_STATUS_ACIN_PRESENT BIT(7) +#define AXP20X_PWR_STATUS_ACIN_AVAIL BIT(6) + +#define AXP813_ACIN_PATH_SEL BIT(7) +#define AXP813_ACIN_PATH_SEL_TO_BIT(x) (!!(x) << 7) + +#define AXP813_VHOLD_MASK GENMASK(5, 3) +#define AXP813_VHOLD_UV_TO_BIT(x) ((((x) / 100000) - 40) << 3) +#define AXP813_VHOLD_REG_TO_UV(x) \ + (((((x) & AXP813_VHOLD_MASK) >> 3) + 40) * 100000) + +#define AXP813_CURR_LIMIT_MASK GENMASK(2, 0) +#define AXP813_CURR_LIMIT_UA_TO_BIT(x) (((x) / 500000) - 3) +#define AXP813_CURR_LIMIT_REG_TO_UA(x) \ + ((((x) & AXP813_CURR_LIMIT_MASK) + 3) * 500000) + +#define DRVNAME "axp20x-ac-power-supply" + +struct axp20x_ac_power { + struct regmap *regmap; + struct power_supply *supply; + struct iio_channel *acin_v; + struct iio_channel *acin_i; + bool has_acin_path_sel; + unsigned int num_irqs; + unsigned int irqs[]; +}; + +static irqreturn_t axp20x_ac_power_irq(int irq, void *devid) +{ + struct axp20x_ac_power *power = devid; + + power_supply_changed(power->supply); + + return IRQ_HANDLED; +} + +static int axp20x_ac_power_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct axp20x_ac_power *power = power_supply_get_drvdata(psy); + int ret, reg; + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, ®); + if (ret) + return ret; + + if (reg & AXP20X_PWR_STATUS_ACIN_PRESENT) { + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return 0; + } + + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + return 0; + + case POWER_SUPPLY_PROP_PRESENT: + ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, ®); + if (ret) + return ret; + + val->intval = !!(reg & AXP20X_PWR_STATUS_ACIN_PRESENT); + return 0; + + case POWER_SUPPLY_PROP_ONLINE: + ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, ®); + if (ret) + return ret; + + val->intval = !!(reg & AXP20X_PWR_STATUS_ACIN_AVAIL); + + /* ACIN_PATH_SEL disables ACIN even if ACIN_AVAIL is set. */ + if (val->intval && power->has_acin_path_sel) { + ret = regmap_read(power->regmap, AXP813_ACIN_PATH_CTRL, + ®); + if (ret) + return ret; + + val->intval = !!(reg & AXP813_ACIN_PATH_SEL); + } + + return 0; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = iio_read_channel_processed(power->acin_v, &val->intval); + if (ret) + return ret; + + /* IIO framework gives mV but Power Supply framework gives uV */ + val->intval *= 1000; + + return 0; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = iio_read_channel_processed(power->acin_i, &val->intval); + if (ret) + return ret; + + /* IIO framework gives mA but Power Supply framework gives uA */ + val->intval *= 1000; + + return 0; + + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + ret = regmap_read(power->regmap, AXP813_ACIN_PATH_CTRL, ®); + if (ret) + return ret; + + val->intval = AXP813_VHOLD_REG_TO_UV(reg); + + return 0; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = regmap_read(power->regmap, AXP813_ACIN_PATH_CTRL, ®); + if (ret) + return ret; + + val->intval = AXP813_CURR_LIMIT_REG_TO_UA(reg); + /* AXP813 datasheet defines values 11x as 4000mA */ + if (val->intval > 4000000) + val->intval = 4000000; + + return 0; + + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp813_ac_power_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct axp20x_ac_power *power = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + return regmap_update_bits(power->regmap, AXP813_ACIN_PATH_CTRL, + AXP813_ACIN_PATH_SEL, + AXP813_ACIN_PATH_SEL_TO_BIT(val->intval)); + + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + if (val->intval < 4000000 || val->intval > 4700000) + return -EINVAL; + + return regmap_update_bits(power->regmap, AXP813_ACIN_PATH_CTRL, + AXP813_VHOLD_MASK, + AXP813_VHOLD_UV_TO_BIT(val->intval)); + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (val->intval < 1500000 || val->intval > 4000000) + return -EINVAL; + + return regmap_update_bits(power->regmap, AXP813_ACIN_PATH_CTRL, + AXP813_CURR_LIMIT_MASK, + AXP813_CURR_LIMIT_UA_TO_BIT(val->intval)); + + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp813_ac_power_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return psp == POWER_SUPPLY_PROP_ONLINE || + psp == POWER_SUPPLY_PROP_VOLTAGE_MIN || + psp == POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT; +} + +static enum power_supply_property axp20x_ac_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static enum power_supply_property axp22x_ac_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, +}; + +static enum power_supply_property axp813_ac_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, +}; + +static const struct power_supply_desc axp20x_ac_power_desc = { + .name = "axp20x-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = axp20x_ac_power_properties, + .num_properties = ARRAY_SIZE(axp20x_ac_power_properties), + .get_property = axp20x_ac_power_get_property, +}; + +static const struct power_supply_desc axp22x_ac_power_desc = { + .name = "axp22x-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = axp22x_ac_power_properties, + .num_properties = ARRAY_SIZE(axp22x_ac_power_properties), + .get_property = axp20x_ac_power_get_property, +}; + +static const struct power_supply_desc axp813_ac_power_desc = { + .name = "axp813-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = axp813_ac_power_properties, + .num_properties = ARRAY_SIZE(axp813_ac_power_properties), + .property_is_writeable = axp813_ac_power_prop_writeable, + .get_property = axp20x_ac_power_get_property, + .set_property = axp813_ac_power_set_property, +}; + +static const char * const axp20x_irq_names[] = { + "ACIN_PLUGIN", + "ACIN_REMOVAL", +}; + +struct axp_data { + const struct power_supply_desc *power_desc; + const char * const *irq_names; + unsigned int num_irq_names; + bool acin_adc; + bool acin_path_sel; +}; + +static const struct axp_data axp20x_data = { + .power_desc = &axp20x_ac_power_desc, + .irq_names = axp20x_irq_names, + .num_irq_names = ARRAY_SIZE(axp20x_irq_names), + .acin_adc = true, + .acin_path_sel = false, +}; + +static const struct axp_data axp22x_data = { + .power_desc = &axp22x_ac_power_desc, + .irq_names = axp20x_irq_names, + .num_irq_names = ARRAY_SIZE(axp20x_irq_names), + .acin_adc = false, + .acin_path_sel = false, +}; + +static const struct axp_data axp813_data = { + .power_desc = &axp813_ac_power_desc, + .irq_names = axp20x_irq_names, + .num_irq_names = ARRAY_SIZE(axp20x_irq_names), + .acin_adc = false, + .acin_path_sel = true, +}; + +#ifdef CONFIG_PM_SLEEP +static int axp20x_ac_power_suspend(struct device *dev) +{ + struct axp20x_ac_power *power = dev_get_drvdata(dev); + int i = 0; + + /* + * Allow wake via ACIN_PLUGIN only. + * + * As nested threaded IRQs are not automatically disabled during + * suspend, we must explicitly disable the remainder of the IRQs. + */ + if (device_may_wakeup(&power->supply->dev)) + enable_irq_wake(power->irqs[i++]); + while (i < power->num_irqs) + disable_irq(power->irqs[i++]); + + return 0; +} + +static int axp20x_ac_power_resume(struct device *dev) +{ + struct axp20x_ac_power *power = dev_get_drvdata(dev); + int i = 0; + + if (device_may_wakeup(&power->supply->dev)) + disable_irq_wake(power->irqs[i++]); + while (i < power->num_irqs) + enable_irq(power->irqs[i++]); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(axp20x_ac_power_pm_ops, axp20x_ac_power_suspend, + axp20x_ac_power_resume); + +static int axp20x_ac_power_probe(struct platform_device *pdev) +{ + struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + struct axp20x_ac_power *power; + const struct axp_data *axp_data; + int i, irq, ret; + + if (!of_device_is_available(pdev->dev.of_node)) + return -ENODEV; + + if (!axp20x) { + dev_err(&pdev->dev, "Parent drvdata not set\n"); + return -EINVAL; + } + + axp_data = of_device_get_match_data(&pdev->dev); + + power = devm_kzalloc(&pdev->dev, + struct_size(power, irqs, axp_data->num_irq_names), + GFP_KERNEL); + if (!power) + return -ENOMEM; + + if (axp_data->acin_adc) { + power->acin_v = devm_iio_channel_get(&pdev->dev, "acin_v"); + if (IS_ERR(power->acin_v)) { + if (PTR_ERR(power->acin_v) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(power->acin_v); + } + + power->acin_i = devm_iio_channel_get(&pdev->dev, "acin_i"); + if (IS_ERR(power->acin_i)) { + if (PTR_ERR(power->acin_i) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(power->acin_i); + } + } + + power->regmap = dev_get_regmap(pdev->dev.parent, NULL); + power->has_acin_path_sel = axp_data->acin_path_sel; + power->num_irqs = axp_data->num_irq_names; + + platform_set_drvdata(pdev, power); + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = power; + + power->supply = devm_power_supply_register(&pdev->dev, + axp_data->power_desc, + &psy_cfg); + if (IS_ERR(power->supply)) + return PTR_ERR(power->supply); + + /* Request irqs after registering, as irqs may trigger immediately */ + for (i = 0; i < axp_data->num_irq_names; i++) { + irq = platform_get_irq_byname(pdev, axp_data->irq_names[i]); + if (irq < 0) { + dev_err(&pdev->dev, "No IRQ for %s: %d\n", + axp_data->irq_names[i], irq); + return irq; + } + power->irqs[i] = regmap_irq_get_virq(axp20x->regmap_irqc, irq); + ret = devm_request_any_context_irq(&pdev->dev, power->irqs[i], + axp20x_ac_power_irq, 0, + DRVNAME, power); + if (ret < 0) { + dev_err(&pdev->dev, "Error requesting %s IRQ: %d\n", + axp_data->irq_names[i], ret); + return ret; + } + } + + return 0; +} + +static const struct of_device_id axp20x_ac_power_match[] = { + { + .compatible = "x-powers,axp202-ac-power-supply", + .data = &axp20x_data, + }, { + .compatible = "x-powers,axp221-ac-power-supply", + .data = &axp22x_data, + }, { + .compatible = "x-powers,axp813-ac-power-supply", + .data = &axp813_data, + }, { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, axp20x_ac_power_match); + +static struct platform_driver axp20x_ac_power_driver = { + .probe = axp20x_ac_power_probe, + .driver = { + .name = DRVNAME, + .of_match_table = axp20x_ac_power_match, + .pm = &axp20x_ac_power_pm_ops, + }, +}; + +module_platform_driver(axp20x_ac_power_driver); + +MODULE_AUTHOR("Quentin Schulz <quentin.schulz@free-electrons.com>"); +MODULE_DESCRIPTION("AXP20X and AXP22X PMICs' AC power supply driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/axp20x_battery.c b/drivers/power/supply/axp20x_battery.c new file mode 100644 index 000000000..9fda98b95 --- /dev/null +++ b/drivers/power/supply/axp20x_battery.c @@ -0,0 +1,647 @@ +/* + * Battery power supply driver for X-Powers AXP20X and AXP22X PMICs + * + * Copyright 2016 Free Electrons NextThing Co. + * Quentin Schulz <quentin.schulz@free-electrons.com> + * + * This driver is based on a previous upstreaming attempt by: + * Bruno Prémont <bonbons@linux-vserver.org> + * + * This file is subject to the terms and conditions of the GNU General + * Public License. See the file "COPYING" in the main directory of this + * archive for more details. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/irq.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/time.h> +#include <linux/iio/iio.h> +#include <linux/iio/consumer.h> +#include <linux/mfd/axp20x.h> + +#define AXP20X_PWR_STATUS_BAT_CHARGING BIT(2) + +#define AXP20X_PWR_OP_BATT_PRESENT BIT(5) +#define AXP20X_PWR_OP_BATT_ACTIVATED BIT(3) + +#define AXP209_FG_PERCENT GENMASK(6, 0) +#define AXP22X_FG_VALID BIT(7) + +#define AXP20X_CHRG_CTRL1_TGT_VOLT GENMASK(6, 5) +#define AXP20X_CHRG_CTRL1_TGT_4_1V (0 << 5) +#define AXP20X_CHRG_CTRL1_TGT_4_15V (1 << 5) +#define AXP20X_CHRG_CTRL1_TGT_4_2V (2 << 5) +#define AXP20X_CHRG_CTRL1_TGT_4_36V (3 << 5) + +#define AXP22X_CHRG_CTRL1_TGT_4_22V (1 << 5) +#define AXP22X_CHRG_CTRL1_TGT_4_24V (3 << 5) + +#define AXP813_CHRG_CTRL1_TGT_4_35V (3 << 5) + +#define AXP20X_CHRG_CTRL1_TGT_CURR GENMASK(3, 0) + +#define AXP20X_V_OFF_MASK GENMASK(2, 0) + +struct axp20x_batt_ps; + +struct axp_data { + int ccc_scale; + int ccc_offset; + bool has_fg_valid; + int (*get_max_voltage)(struct axp20x_batt_ps *batt, int *val); + int (*set_max_voltage)(struct axp20x_batt_ps *batt, int val); +}; + +struct axp20x_batt_ps { + struct regmap *regmap; + struct power_supply *batt; + struct device *dev; + struct iio_channel *batt_chrg_i; + struct iio_channel *batt_dischrg_i; + struct iio_channel *batt_v; + /* Maximum constant charge current */ + unsigned int max_ccc; + const struct axp_data *data; +}; + +static int axp20x_battery_get_max_voltage(struct axp20x_batt_ps *axp20x_batt, + int *val) +{ + int ret, reg; + + ret = regmap_read(axp20x_batt->regmap, AXP20X_CHRG_CTRL1, ®); + if (ret) + return ret; + + switch (reg & AXP20X_CHRG_CTRL1_TGT_VOLT) { + case AXP20X_CHRG_CTRL1_TGT_4_1V: + *val = 4100000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_15V: + *val = 4150000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_2V: + *val = 4200000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_36V: + *val = 4360000; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int axp22x_battery_get_max_voltage(struct axp20x_batt_ps *axp20x_batt, + int *val) +{ + int ret, reg; + + ret = regmap_read(axp20x_batt->regmap, AXP20X_CHRG_CTRL1, ®); + if (ret) + return ret; + + switch (reg & AXP20X_CHRG_CTRL1_TGT_VOLT) { + case AXP20X_CHRG_CTRL1_TGT_4_1V: + *val = 4100000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_2V: + *val = 4200000; + break; + case AXP22X_CHRG_CTRL1_TGT_4_22V: + *val = 4220000; + break; + case AXP22X_CHRG_CTRL1_TGT_4_24V: + *val = 4240000; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int axp813_battery_get_max_voltage(struct axp20x_batt_ps *axp20x_batt, + int *val) +{ + int ret, reg; + + ret = regmap_read(axp20x_batt->regmap, AXP20X_CHRG_CTRL1, ®); + if (ret) + return ret; + + switch (reg & AXP20X_CHRG_CTRL1_TGT_VOLT) { + case AXP20X_CHRG_CTRL1_TGT_4_1V: + *val = 4100000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_15V: + *val = 4150000; + break; + case AXP20X_CHRG_CTRL1_TGT_4_2V: + *val = 4200000; + break; + case AXP813_CHRG_CTRL1_TGT_4_35V: + *val = 4350000; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int axp20x_get_constant_charge_current(struct axp20x_batt_ps *axp, + int *val) +{ + int ret; + + ret = regmap_read(axp->regmap, AXP20X_CHRG_CTRL1, val); + if (ret) + return ret; + + *val &= AXP20X_CHRG_CTRL1_TGT_CURR; + + *val = *val * axp->data->ccc_scale + axp->data->ccc_offset; + + return 0; +} + +static int axp20x_battery_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct axp20x_batt_ps *axp20x_batt = power_supply_get_drvdata(psy); + int ret = 0, reg, val1; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + case POWER_SUPPLY_PROP_ONLINE: + ret = regmap_read(axp20x_batt->regmap, AXP20X_PWR_OP_MODE, + ®); + if (ret) + return ret; + + val->intval = !!(reg & AXP20X_PWR_OP_BATT_PRESENT); + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = regmap_read(axp20x_batt->regmap, AXP20X_PWR_INPUT_STATUS, + ®); + if (ret) + return ret; + + if (reg & AXP20X_PWR_STATUS_BAT_CHARGING) { + val->intval = POWER_SUPPLY_STATUS_CHARGING; + return 0; + } + + ret = iio_read_channel_processed(axp20x_batt->batt_dischrg_i, + &val1); + if (ret) + return ret; + + if (val1) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + ret = regmap_read(axp20x_batt->regmap, AXP20X_FG_RES, &val1); + if (ret) + return ret; + + /* + * Fuel Gauge data takes 7 bits but the stored value seems to be + * directly the raw percentage without any scaling to 7 bits. + */ + if ((val1 & AXP209_FG_PERCENT) == 100) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + + case POWER_SUPPLY_PROP_HEALTH: + ret = regmap_read(axp20x_batt->regmap, AXP20X_PWR_OP_MODE, + &val1); + if (ret) + return ret; + + if (val1 & AXP20X_PWR_OP_BATT_ACTIVATED) { + val->intval = POWER_SUPPLY_HEALTH_DEAD; + return 0; + } + + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = axp20x_get_constant_charge_current(axp20x_batt, + &val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = axp20x_batt->max_ccc; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = regmap_read(axp20x_batt->regmap, AXP20X_PWR_INPUT_STATUS, + ®); + if (ret) + return ret; + + if (reg & AXP20X_PWR_STATUS_BAT_CHARGING) { + ret = iio_read_channel_processed(axp20x_batt->batt_chrg_i, &val->intval); + } else { + ret = iio_read_channel_processed(axp20x_batt->batt_dischrg_i, &val1); + val->intval = -val1; + } + if (ret) + return ret; + + /* IIO framework gives mA but Power Supply framework gives uA */ + val->intval *= 1000; + break; + + case POWER_SUPPLY_PROP_CAPACITY: + /* When no battery is present, return capacity is 100% */ + ret = regmap_read(axp20x_batt->regmap, AXP20X_PWR_OP_MODE, + ®); + if (ret) + return ret; + + if (!(reg & AXP20X_PWR_OP_BATT_PRESENT)) { + val->intval = 100; + return 0; + } + + ret = regmap_read(axp20x_batt->regmap, AXP20X_FG_RES, ®); + if (ret) + return ret; + + if (axp20x_batt->data->has_fg_valid && !(reg & AXP22X_FG_VALID)) + return -EINVAL; + + /* + * Fuel Gauge data takes 7 bits but the stored value seems to be + * directly the raw percentage without any scaling to 7 bits. + */ + val->intval = reg & AXP209_FG_PERCENT; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + return axp20x_batt->data->get_max_voltage(axp20x_batt, + &val->intval); + + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + ret = regmap_read(axp20x_batt->regmap, AXP20X_V_OFF, ®); + if (ret) + return ret; + + val->intval = 2600000 + 100000 * (reg & AXP20X_V_OFF_MASK); + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = iio_read_channel_processed(axp20x_batt->batt_v, + &val->intval); + if (ret) + return ret; + + /* IIO framework gives mV but Power Supply framework gives uV */ + val->intval *= 1000; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int axp22x_battery_set_max_voltage(struct axp20x_batt_ps *axp20x_batt, + int val) +{ + switch (val) { + case 4100000: + val = AXP20X_CHRG_CTRL1_TGT_4_1V; + break; + + case 4200000: + val = AXP20X_CHRG_CTRL1_TGT_4_2V; + break; + + default: + /* + * AXP20x max voltage can be set to 4.36V and AXP22X max voltage + * can be set to 4.22V and 4.24V, but these voltages are too + * high for Lithium based batteries (AXP PMICs are supposed to + * be used with these kinds of battery). + */ + return -EINVAL; + } + + return regmap_update_bits(axp20x_batt->regmap, AXP20X_CHRG_CTRL1, + AXP20X_CHRG_CTRL1_TGT_VOLT, val); +} + +static int axp20x_battery_set_max_voltage(struct axp20x_batt_ps *axp20x_batt, + int val) +{ + switch (val) { + case 4100000: + val = AXP20X_CHRG_CTRL1_TGT_4_1V; + break; + + case 4150000: + val = AXP20X_CHRG_CTRL1_TGT_4_15V; + break; + + case 4200000: + val = AXP20X_CHRG_CTRL1_TGT_4_2V; + break; + + default: + /* + * AXP20x max voltage can be set to 4.36V and AXP22X max voltage + * can be set to 4.22V and 4.24V, but these voltages are too + * high for Lithium based batteries (AXP PMICs are supposed to + * be used with these kinds of battery). + */ + return -EINVAL; + } + + return regmap_update_bits(axp20x_batt->regmap, AXP20X_CHRG_CTRL1, + AXP20X_CHRG_CTRL1_TGT_VOLT, val); +} + +static int axp20x_set_constant_charge_current(struct axp20x_batt_ps *axp_batt, + int charge_current) +{ + if (charge_current > axp_batt->max_ccc) + return -EINVAL; + + charge_current = (charge_current - axp_batt->data->ccc_offset) / + axp_batt->data->ccc_scale; + + if (charge_current > AXP20X_CHRG_CTRL1_TGT_CURR || charge_current < 0) + return -EINVAL; + + return regmap_update_bits(axp_batt->regmap, AXP20X_CHRG_CTRL1, + AXP20X_CHRG_CTRL1_TGT_CURR, charge_current); +} + +static int axp20x_set_max_constant_charge_current(struct axp20x_batt_ps *axp, + int charge_current) +{ + bool lower_max = false; + + charge_current = (charge_current - axp->data->ccc_offset) / + axp->data->ccc_scale; + + if (charge_current > AXP20X_CHRG_CTRL1_TGT_CURR || charge_current < 0) + return -EINVAL; + + charge_current = charge_current * axp->data->ccc_scale + + axp->data->ccc_offset; + + if (charge_current > axp->max_ccc) + dev_warn(axp->dev, + "Setting max constant charge current higher than previously defined. Note that increasing the constant charge current may damage your battery.\n"); + else + lower_max = true; + + axp->max_ccc = charge_current; + + if (lower_max) { + int current_cc; + + axp20x_get_constant_charge_current(axp, ¤t_cc); + if (current_cc > charge_current) + axp20x_set_constant_charge_current(axp, charge_current); + } + + return 0; +} +static int axp20x_set_voltage_min_design(struct axp20x_batt_ps *axp_batt, + int min_voltage) +{ + int val1 = (min_voltage - 2600000) / 100000; + + if (val1 < 0 || val1 > AXP20X_V_OFF_MASK) + return -EINVAL; + + return regmap_update_bits(axp_batt->regmap, AXP20X_V_OFF, + AXP20X_V_OFF_MASK, val1); +} + +static int axp20x_battery_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct axp20x_batt_ps *axp20x_batt = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + return axp20x_set_voltage_min_design(axp20x_batt, val->intval); + + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + return axp20x_batt->data->set_max_voltage(axp20x_batt, val->intval); + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return axp20x_set_constant_charge_current(axp20x_batt, + val->intval); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + return axp20x_set_max_constant_charge_current(axp20x_batt, + val->intval); + + default: + return -EINVAL; + } +} + +static enum power_supply_property axp20x_battery_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_CAPACITY, +}; + +static int axp20x_battery_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return psp == POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN || + psp == POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN || + psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT || + psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX; +} + +static const struct power_supply_desc axp20x_batt_ps_desc = { + .name = "axp20x-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = axp20x_battery_props, + .num_properties = ARRAY_SIZE(axp20x_battery_props), + .property_is_writeable = axp20x_battery_prop_writeable, + .get_property = axp20x_battery_get_prop, + .set_property = axp20x_battery_set_prop, +}; + +static const struct axp_data axp209_data = { + .ccc_scale = 100000, + .ccc_offset = 300000, + .get_max_voltage = axp20x_battery_get_max_voltage, + .set_max_voltage = axp20x_battery_set_max_voltage, +}; + +static const struct axp_data axp221_data = { + .ccc_scale = 150000, + .ccc_offset = 300000, + .has_fg_valid = true, + .get_max_voltage = axp22x_battery_get_max_voltage, + .set_max_voltage = axp22x_battery_set_max_voltage, +}; + +static const struct axp_data axp813_data = { + .ccc_scale = 200000, + .ccc_offset = 200000, + .has_fg_valid = true, + .get_max_voltage = axp813_battery_get_max_voltage, + .set_max_voltage = axp20x_battery_set_max_voltage, +}; + +static const struct of_device_id axp20x_battery_ps_id[] = { + { + .compatible = "x-powers,axp209-battery-power-supply", + .data = (void *)&axp209_data, + }, { + .compatible = "x-powers,axp221-battery-power-supply", + .data = (void *)&axp221_data, + }, { + .compatible = "x-powers,axp813-battery-power-supply", + .data = (void *)&axp813_data, + }, { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, axp20x_battery_ps_id); + +static int axp20x_power_probe(struct platform_device *pdev) +{ + struct axp20x_batt_ps *axp20x_batt; + struct power_supply_config psy_cfg = {}; + struct power_supply_battery_info info; + struct device *dev = &pdev->dev; + + if (!of_device_is_available(pdev->dev.of_node)) + return -ENODEV; + + axp20x_batt = devm_kzalloc(&pdev->dev, sizeof(*axp20x_batt), + GFP_KERNEL); + if (!axp20x_batt) + return -ENOMEM; + + axp20x_batt->dev = &pdev->dev; + + axp20x_batt->batt_v = devm_iio_channel_get(&pdev->dev, "batt_v"); + if (IS_ERR(axp20x_batt->batt_v)) { + if (PTR_ERR(axp20x_batt->batt_v) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(axp20x_batt->batt_v); + } + + axp20x_batt->batt_chrg_i = devm_iio_channel_get(&pdev->dev, + "batt_chrg_i"); + if (IS_ERR(axp20x_batt->batt_chrg_i)) { + if (PTR_ERR(axp20x_batt->batt_chrg_i) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(axp20x_batt->batt_chrg_i); + } + + axp20x_batt->batt_dischrg_i = devm_iio_channel_get(&pdev->dev, + "batt_dischrg_i"); + if (IS_ERR(axp20x_batt->batt_dischrg_i)) { + if (PTR_ERR(axp20x_batt->batt_dischrg_i) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(axp20x_batt->batt_dischrg_i); + } + + axp20x_batt->regmap = dev_get_regmap(pdev->dev.parent, NULL); + platform_set_drvdata(pdev, axp20x_batt); + + psy_cfg.drv_data = axp20x_batt; + psy_cfg.of_node = pdev->dev.of_node; + + axp20x_batt->data = (struct axp_data *)of_device_get_match_data(dev); + + axp20x_batt->batt = devm_power_supply_register(&pdev->dev, + &axp20x_batt_ps_desc, + &psy_cfg); + if (IS_ERR(axp20x_batt->batt)) { + dev_err(&pdev->dev, "failed to register power supply: %ld\n", + PTR_ERR(axp20x_batt->batt)); + return PTR_ERR(axp20x_batt->batt); + } + + if (!power_supply_get_battery_info(axp20x_batt->batt, &info)) { + int vmin = info.voltage_min_design_uv; + int ccc = info.constant_charge_current_max_ua; + + if (vmin > 0 && axp20x_set_voltage_min_design(axp20x_batt, + vmin)) + dev_err(&pdev->dev, + "couldn't set voltage_min_design\n"); + + /* Set max to unverified value to be able to set CCC */ + axp20x_batt->max_ccc = ccc; + + if (ccc <= 0 || axp20x_set_constant_charge_current(axp20x_batt, + ccc)) { + dev_err(&pdev->dev, + "couldn't set constant charge current from DT: fallback to minimum value\n"); + ccc = 300000; + axp20x_batt->max_ccc = ccc; + axp20x_set_constant_charge_current(axp20x_batt, ccc); + } + } + + /* + * Update max CCC to a valid value if battery info is present or set it + * to current register value by default. + */ + axp20x_get_constant_charge_current(axp20x_batt, + &axp20x_batt->max_ccc); + + return 0; +} + +static struct platform_driver axp20x_batt_driver = { + .probe = axp20x_power_probe, + .driver = { + .name = "axp20x-battery-power-supply", + .of_match_table = axp20x_battery_ps_id, + }, +}; + +module_platform_driver(axp20x_batt_driver); + +MODULE_DESCRIPTION("Battery power supply driver for AXP20X and AXP22X PMICs"); +MODULE_AUTHOR("Quentin Schulz <quentin.schulz@free-electrons.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/axp20x_usb_power.c b/drivers/power/supply/axp20x_usb_power.c new file mode 100644 index 000000000..25e288388 --- /dev/null +++ b/drivers/power/supply/axp20x_usb_power.c @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * AXP20x PMIC USB power supply status driver + * + * Copyright (C) 2015 Hans de Goede <hdegoede@redhat.com> + * Copyright (C) 2014 Bruno Prémont <bonbons@linux-vserver.org> + */ + +#include <linux/bitops.h> +#include <linux/device.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/mfd/axp20x.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/iio/consumer.h> +#include <linux/workqueue.h> + +#define DRVNAME "axp20x-usb-power-supply" + +#define AXP20X_PWR_STATUS_VBUS_PRESENT BIT(5) +#define AXP20X_PWR_STATUS_VBUS_USED BIT(4) + +#define AXP20X_USB_STATUS_VBUS_VALID BIT(2) + +#define AXP20X_VBUS_PATH_SEL BIT(7) +#define AXP20X_VBUS_PATH_SEL_OFFSET 7 + +#define AXP20X_VBUS_VHOLD_uV(b) (4000000 + (((b) >> 3) & 7) * 100000) +#define AXP20X_VBUS_VHOLD_MASK GENMASK(5, 3) +#define AXP20X_VBUS_VHOLD_OFFSET 3 +#define AXP20X_VBUS_CLIMIT_MASK 3 +#define AXP20X_VBUS_CLIMIT_900mA 0 +#define AXP20X_VBUS_CLIMIT_500mA 1 +#define AXP20X_VBUS_CLIMIT_100mA 2 +#define AXP20X_VBUS_CLIMIT_NONE 3 + +#define AXP813_VBUS_CLIMIT_900mA 0 +#define AXP813_VBUS_CLIMIT_1500mA 1 +#define AXP813_VBUS_CLIMIT_2000mA 2 +#define AXP813_VBUS_CLIMIT_2500mA 3 + +#define AXP20X_ADC_EN1_VBUS_CURR BIT(2) +#define AXP20X_ADC_EN1_VBUS_VOLT BIT(3) + +#define AXP20X_VBUS_MON_VBUS_VALID BIT(3) + +#define AXP813_BC_EN BIT(0) + +/* + * Note do not raise the debounce time, we must report Vusb high within + * 100ms otherwise we get Vbus errors in musb. + */ +#define DEBOUNCE_TIME msecs_to_jiffies(50) + +struct axp20x_usb_power { + struct regmap *regmap; + struct power_supply *supply; + enum axp20x_variants axp20x_id; + struct iio_channel *vbus_v; + struct iio_channel *vbus_i; + struct delayed_work vbus_detect; + unsigned int old_status; + unsigned int online; + unsigned int num_irqs; + unsigned int irqs[]; +}; + +static bool axp20x_usb_vbus_needs_polling(struct axp20x_usb_power *power) +{ + /* + * Polling is only necessary while VBUS is offline. While online, a + * present->absent transition implies an online->offline transition + * and will trigger the VBUS_REMOVAL IRQ. + */ + if (power->axp20x_id >= AXP221_ID && !power->online) + return true; + + return false; +} + +static irqreturn_t axp20x_usb_power_irq(int irq, void *devid) +{ + struct axp20x_usb_power *power = devid; + + power_supply_changed(power->supply); + + mod_delayed_work(system_wq, &power->vbus_detect, DEBOUNCE_TIME); + + return IRQ_HANDLED; +} + +static void axp20x_usb_power_poll_vbus(struct work_struct *work) +{ + struct axp20x_usb_power *power = + container_of(work, struct axp20x_usb_power, vbus_detect.work); + unsigned int val; + int ret; + + ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, &val); + if (ret) + goto out; + + val &= (AXP20X_PWR_STATUS_VBUS_PRESENT | AXP20X_PWR_STATUS_VBUS_USED); + if (val != power->old_status) + power_supply_changed(power->supply); + + power->old_status = val; + power->online = val & AXP20X_PWR_STATUS_VBUS_USED; + +out: + if (axp20x_usb_vbus_needs_polling(power)) + mod_delayed_work(system_wq, &power->vbus_detect, DEBOUNCE_TIME); +} + +static int axp20x_get_current_max(struct axp20x_usb_power *power, int *val) +{ + unsigned int v; + int ret = regmap_read(power->regmap, AXP20X_VBUS_IPSOUT_MGMT, &v); + + if (ret) + return ret; + + switch (v & AXP20X_VBUS_CLIMIT_MASK) { + case AXP20X_VBUS_CLIMIT_100mA: + if (power->axp20x_id == AXP221_ID) + *val = -1; /* No 100mA limit */ + else + *val = 100000; + break; + case AXP20X_VBUS_CLIMIT_500mA: + *val = 500000; + break; + case AXP20X_VBUS_CLIMIT_900mA: + *val = 900000; + break; + case AXP20X_VBUS_CLIMIT_NONE: + *val = -1; + break; + } + + return 0; +} + +static int axp813_get_current_max(struct axp20x_usb_power *power, int *val) +{ + unsigned int v; + int ret = regmap_read(power->regmap, AXP20X_VBUS_IPSOUT_MGMT, &v); + + if (ret) + return ret; + + switch (v & AXP20X_VBUS_CLIMIT_MASK) { + case AXP813_VBUS_CLIMIT_900mA: + *val = 900000; + break; + case AXP813_VBUS_CLIMIT_1500mA: + *val = 1500000; + break; + case AXP813_VBUS_CLIMIT_2000mA: + *val = 2000000; + break; + case AXP813_VBUS_CLIMIT_2500mA: + *val = 2500000; + break; + } + return 0; +} + +static int axp20x_usb_power_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct axp20x_usb_power *power = power_supply_get_drvdata(psy); + unsigned int input, v; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + ret = regmap_read(power->regmap, AXP20X_VBUS_IPSOUT_MGMT, &v); + if (ret) + return ret; + + val->intval = AXP20X_VBUS_VHOLD_uV(v); + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (IS_ENABLED(CONFIG_AXP20X_ADC)) { + ret = iio_read_channel_processed(power->vbus_v, + &val->intval); + if (ret) + return ret; + + /* + * IIO framework gives mV but Power Supply framework + * gives uV. + */ + val->intval *= 1000; + return 0; + } + + ret = axp20x_read_variable_width(power->regmap, + AXP20X_VBUS_V_ADC_H, 12); + if (ret < 0) + return ret; + + val->intval = ret * 1700; /* 1 step = 1.7 mV */ + return 0; + case POWER_SUPPLY_PROP_CURRENT_MAX: + if (power->axp20x_id == AXP813_ID) + return axp813_get_current_max(power, &val->intval); + return axp20x_get_current_max(power, &val->intval); + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (IS_ENABLED(CONFIG_AXP20X_ADC)) { + ret = iio_read_channel_processed(power->vbus_i, + &val->intval); + if (ret) + return ret; + + /* + * IIO framework gives mA but Power Supply framework + * gives uA. + */ + val->intval *= 1000; + return 0; + } + + ret = axp20x_read_variable_width(power->regmap, + AXP20X_VBUS_I_ADC_H, 12); + if (ret < 0) + return ret; + + val->intval = ret * 375; /* 1 step = 0.375 mA */ + return 0; + default: + break; + } + + /* All the properties below need the input-status reg value */ + ret = regmap_read(power->regmap, AXP20X_PWR_INPUT_STATUS, &input); + if (ret) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + if (!(input & AXP20X_PWR_STATUS_VBUS_PRESENT)) { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + break; + } + + val->intval = POWER_SUPPLY_HEALTH_GOOD; + + if (power->axp20x_id == AXP202_ID) { + ret = regmap_read(power->regmap, + AXP20X_USB_OTG_STATUS, &v); + if (ret) + return ret; + + if (!(v & AXP20X_USB_STATUS_VBUS_VALID)) + val->intval = + POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + } + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(input & AXP20X_PWR_STATUS_VBUS_PRESENT); + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = !!(input & AXP20X_PWR_STATUS_VBUS_USED); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int axp813_usb_power_set_online(struct axp20x_usb_power *power, + int intval) +{ + int val = !intval << AXP20X_VBUS_PATH_SEL_OFFSET; + + return regmap_update_bits(power->regmap, + AXP20X_VBUS_IPSOUT_MGMT, + AXP20X_VBUS_PATH_SEL, val); +} + +static int axp20x_usb_power_set_voltage_min(struct axp20x_usb_power *power, + int intval) +{ + int val; + + switch (intval) { + case 4000000: + case 4100000: + case 4200000: + case 4300000: + case 4400000: + case 4500000: + case 4600000: + case 4700000: + val = (intval - 4000000) / 100000; + return regmap_update_bits(power->regmap, + AXP20X_VBUS_IPSOUT_MGMT, + AXP20X_VBUS_VHOLD_MASK, + val << AXP20X_VBUS_VHOLD_OFFSET); + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp813_usb_power_set_current_max(struct axp20x_usb_power *power, + int intval) +{ + int val; + + switch (intval) { + case 900000: + return regmap_update_bits(power->regmap, + AXP20X_VBUS_IPSOUT_MGMT, + AXP20X_VBUS_CLIMIT_MASK, + AXP813_VBUS_CLIMIT_900mA); + case 1500000: + case 2000000: + case 2500000: + val = (intval - 1000000) / 500000; + return regmap_update_bits(power->regmap, + AXP20X_VBUS_IPSOUT_MGMT, + AXP20X_VBUS_CLIMIT_MASK, val); + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp20x_usb_power_set_current_max(struct axp20x_usb_power *power, + int intval) +{ + int val; + + switch (intval) { + case 100000: + if (power->axp20x_id == AXP221_ID) + return -EINVAL; + fallthrough; + case 500000: + case 900000: + val = (900000 - intval) / 400000; + return regmap_update_bits(power->regmap, + AXP20X_VBUS_IPSOUT_MGMT, + AXP20X_VBUS_CLIMIT_MASK, val); + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp20x_usb_power_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct axp20x_usb_power *power = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + if (power->axp20x_id != AXP813_ID) + return -EINVAL; + return axp813_usb_power_set_online(power, val->intval); + + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + return axp20x_usb_power_set_voltage_min(power, val->intval); + + case POWER_SUPPLY_PROP_CURRENT_MAX: + if (power->axp20x_id == AXP813_ID) + return axp813_usb_power_set_current_max(power, + val->intval); + return axp20x_usb_power_set_current_max(power, val->intval); + + default: + return -EINVAL; + } + + return -EINVAL; +} + +static int axp20x_usb_power_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + struct axp20x_usb_power *power = power_supply_get_drvdata(psy); + + /* + * The VBUS path select flag works differently on on AXP288 and newer: + * - On AXP20x and AXP22x, the flag enables VBUS (ignoring N_VBUSEN). + * - On AXP288 and AXP8xx, the flag disables VBUS (ignoring N_VBUSEN). + * We only expose the control on variants where it can be used to force + * the VBUS input offline. + */ + if (psp == POWER_SUPPLY_PROP_ONLINE) + return power->axp20x_id == AXP813_ID; + + return psp == POWER_SUPPLY_PROP_VOLTAGE_MIN || + psp == POWER_SUPPLY_PROP_CURRENT_MAX; +} + +static enum power_supply_property axp20x_usb_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static enum power_supply_property axp22x_usb_power_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static const struct power_supply_desc axp20x_usb_power_desc = { + .name = "axp20x-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = axp20x_usb_power_properties, + .num_properties = ARRAY_SIZE(axp20x_usb_power_properties), + .property_is_writeable = axp20x_usb_power_prop_writeable, + .get_property = axp20x_usb_power_get_property, + .set_property = axp20x_usb_power_set_property, +}; + +static const struct power_supply_desc axp22x_usb_power_desc = { + .name = "axp20x-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = axp22x_usb_power_properties, + .num_properties = ARRAY_SIZE(axp22x_usb_power_properties), + .property_is_writeable = axp20x_usb_power_prop_writeable, + .get_property = axp20x_usb_power_get_property, + .set_property = axp20x_usb_power_set_property, +}; + +static const char * const axp20x_irq_names[] = { + "VBUS_PLUGIN", + "VBUS_REMOVAL", + "VBUS_VALID", + "VBUS_NOT_VALID", +}; + +static const char * const axp22x_irq_names[] = { + "VBUS_PLUGIN", + "VBUS_REMOVAL", +}; + +struct axp_data { + const struct power_supply_desc *power_desc; + const char * const *irq_names; + unsigned int num_irq_names; + enum axp20x_variants axp20x_id; +}; + +static const struct axp_data axp202_data = { + .power_desc = &axp20x_usb_power_desc, + .irq_names = axp20x_irq_names, + .num_irq_names = ARRAY_SIZE(axp20x_irq_names), + .axp20x_id = AXP202_ID, +}; + +static const struct axp_data axp221_data = { + .power_desc = &axp22x_usb_power_desc, + .irq_names = axp22x_irq_names, + .num_irq_names = ARRAY_SIZE(axp22x_irq_names), + .axp20x_id = AXP221_ID, +}; + +static const struct axp_data axp223_data = { + .power_desc = &axp22x_usb_power_desc, + .irq_names = axp22x_irq_names, + .num_irq_names = ARRAY_SIZE(axp22x_irq_names), + .axp20x_id = AXP223_ID, +}; + +static const struct axp_data axp813_data = { + .power_desc = &axp22x_usb_power_desc, + .irq_names = axp22x_irq_names, + .num_irq_names = ARRAY_SIZE(axp22x_irq_names), + .axp20x_id = AXP813_ID, +}; + +#ifdef CONFIG_PM_SLEEP +static int axp20x_usb_power_suspend(struct device *dev) +{ + struct axp20x_usb_power *power = dev_get_drvdata(dev); + int i = 0; + + /* + * Allow wake via VBUS_PLUGIN only. + * + * As nested threaded IRQs are not automatically disabled during + * suspend, we must explicitly disable the remainder of the IRQs. + */ + if (device_may_wakeup(&power->supply->dev)) + enable_irq_wake(power->irqs[i++]); + while (i < power->num_irqs) + disable_irq(power->irqs[i++]); + + return 0; +} + +static int axp20x_usb_power_resume(struct device *dev) +{ + struct axp20x_usb_power *power = dev_get_drvdata(dev); + int i = 0; + + if (device_may_wakeup(&power->supply->dev)) + disable_irq_wake(power->irqs[i++]); + while (i < power->num_irqs) + enable_irq(power->irqs[i++]); + + mod_delayed_work(system_wq, &power->vbus_detect, DEBOUNCE_TIME); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(axp20x_usb_power_pm_ops, axp20x_usb_power_suspend, + axp20x_usb_power_resume); + +static int configure_iio_channels(struct platform_device *pdev, + struct axp20x_usb_power *power) +{ + power->vbus_v = devm_iio_channel_get(&pdev->dev, "vbus_v"); + if (IS_ERR(power->vbus_v)) { + if (PTR_ERR(power->vbus_v) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(power->vbus_v); + } + + power->vbus_i = devm_iio_channel_get(&pdev->dev, "vbus_i"); + if (IS_ERR(power->vbus_i)) { + if (PTR_ERR(power->vbus_i) == -ENODEV) + return -EPROBE_DEFER; + return PTR_ERR(power->vbus_i); + } + + return 0; +} + +static int configure_adc_registers(struct axp20x_usb_power *power) +{ + /* Enable vbus voltage and current measurement */ + return regmap_update_bits(power->regmap, AXP20X_ADC_EN1, + AXP20X_ADC_EN1_VBUS_CURR | + AXP20X_ADC_EN1_VBUS_VOLT, + AXP20X_ADC_EN1_VBUS_CURR | + AXP20X_ADC_EN1_VBUS_VOLT); +} + +static int axp20x_usb_power_probe(struct platform_device *pdev) +{ + struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + struct axp20x_usb_power *power; + const struct axp_data *axp_data; + int i, irq, ret; + + if (!of_device_is_available(pdev->dev.of_node)) + return -ENODEV; + + if (!axp20x) { + dev_err(&pdev->dev, "Parent drvdata not set\n"); + return -EINVAL; + } + + axp_data = of_device_get_match_data(&pdev->dev); + + power = devm_kzalloc(&pdev->dev, + struct_size(power, irqs, axp_data->num_irq_names), + GFP_KERNEL); + if (!power) + return -ENOMEM; + + platform_set_drvdata(pdev, power); + + power->axp20x_id = axp_data->axp20x_id; + power->regmap = axp20x->regmap; + power->num_irqs = axp_data->num_irq_names; + INIT_DELAYED_WORK(&power->vbus_detect, axp20x_usb_power_poll_vbus); + + if (power->axp20x_id == AXP202_ID) { + /* Enable vbus valid checking */ + ret = regmap_update_bits(power->regmap, AXP20X_VBUS_MON, + AXP20X_VBUS_MON_VBUS_VALID, + AXP20X_VBUS_MON_VBUS_VALID); + if (ret) + return ret; + + if (IS_ENABLED(CONFIG_AXP20X_ADC)) + ret = configure_iio_channels(pdev, power); + else + ret = configure_adc_registers(power); + + if (ret) + return ret; + } + + if (power->axp20x_id == AXP813_ID) { + /* Enable USB Battery Charging specification detection */ + regmap_update_bits(axp20x->regmap, AXP288_BC_GLOBAL, + AXP813_BC_EN, AXP813_BC_EN); + } + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = power; + + power->supply = devm_power_supply_register(&pdev->dev, + axp_data->power_desc, + &psy_cfg); + if (IS_ERR(power->supply)) + return PTR_ERR(power->supply); + + /* Request irqs after registering, as irqs may trigger immediately */ + for (i = 0; i < axp_data->num_irq_names; i++) { + irq = platform_get_irq_byname(pdev, axp_data->irq_names[i]); + if (irq < 0) { + dev_err(&pdev->dev, "No IRQ for %s: %d\n", + axp_data->irq_names[i], irq); + return irq; + } + power->irqs[i] = regmap_irq_get_virq(axp20x->regmap_irqc, irq); + ret = devm_request_any_context_irq(&pdev->dev, power->irqs[i], + axp20x_usb_power_irq, 0, + DRVNAME, power); + if (ret < 0) { + dev_err(&pdev->dev, "Error requesting %s IRQ: %d\n", + axp_data->irq_names[i], ret); + return ret; + } + } + + if (axp20x_usb_vbus_needs_polling(power)) + queue_delayed_work(system_wq, &power->vbus_detect, 0); + + return 0; +} + +static int axp20x_usb_power_remove(struct platform_device *pdev) +{ + struct axp20x_usb_power *power = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&power->vbus_detect); + + return 0; +} + +static const struct of_device_id axp20x_usb_power_match[] = { + { + .compatible = "x-powers,axp202-usb-power-supply", + .data = &axp202_data, + }, { + .compatible = "x-powers,axp221-usb-power-supply", + .data = &axp221_data, + }, { + .compatible = "x-powers,axp223-usb-power-supply", + .data = &axp223_data, + }, { + .compatible = "x-powers,axp813-usb-power-supply", + .data = &axp813_data, + }, { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, axp20x_usb_power_match); + +static struct platform_driver axp20x_usb_power_driver = { + .probe = axp20x_usb_power_probe, + .remove = axp20x_usb_power_remove, + .driver = { + .name = DRVNAME, + .of_match_table = axp20x_usb_power_match, + .pm = &axp20x_usb_power_pm_ops, + }, +}; + +module_platform_driver(axp20x_usb_power_driver); + +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_DESCRIPTION("AXP20x PMIC USB power supply status driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/axp288_charger.c b/drivers/power/supply/axp288_charger.c new file mode 100644 index 000000000..f65bf7b29 --- /dev/null +++ b/drivers/power/supply/axp288_charger.c @@ -0,0 +1,939 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * axp288_charger.c - X-power AXP288 PMIC Charger driver + * + * Copyright (C) 2016-2017 Hans de Goede <hdegoede@redhat.com> + * Copyright (C) 2014 Intel Corporation + * Author: Ramakrishna Pallala <ramakrishna.pallala@intel.com> + */ + +#include <linux/acpi.h> +#include <linux/bitops.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/regmap.h> +#include <linux/workqueue.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/usb/otg.h> +#include <linux/notifier.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/mfd/axp20x.h> +#include <linux/extcon.h> +#include <linux/dmi.h> + +#define PS_STAT_VBUS_TRIGGER BIT(0) +#define PS_STAT_BAT_CHRG_DIR BIT(2) +#define PS_STAT_VBAT_ABOVE_VHOLD BIT(3) +#define PS_STAT_VBUS_VALID BIT(4) +#define PS_STAT_VBUS_PRESENT BIT(5) + +#define CHRG_STAT_BAT_SAFE_MODE BIT(3) +#define CHRG_STAT_BAT_VALID BIT(4) +#define CHRG_STAT_BAT_PRESENT BIT(5) +#define CHRG_STAT_CHARGING BIT(6) +#define CHRG_STAT_PMIC_OTP BIT(7) + +#define VBUS_ISPOUT_CUR_LIM_MASK 0x03 +#define VBUS_ISPOUT_CUR_LIM_BIT_POS 0 +#define VBUS_ISPOUT_CUR_LIM_900MA 0x0 /* 900mA */ +#define VBUS_ISPOUT_CUR_LIM_1500MA 0x1 /* 1500mA */ +#define VBUS_ISPOUT_CUR_LIM_2000MA 0x2 /* 2000mA */ +#define VBUS_ISPOUT_CUR_NO_LIM 0x3 /* 2500mA */ +#define VBUS_ISPOUT_VHOLD_SET_MASK 0x38 +#define VBUS_ISPOUT_VHOLD_SET_BIT_POS 0x3 +#define VBUS_ISPOUT_VHOLD_SET_OFFSET 4000 /* 4000mV */ +#define VBUS_ISPOUT_VHOLD_SET_LSB_RES 100 /* 100mV */ +#define VBUS_ISPOUT_VHOLD_SET_4400MV 0x4 /* 4400mV */ +#define VBUS_ISPOUT_VBUS_PATH_DIS BIT(7) + +#define CHRG_CCCV_CC_MASK 0xf /* 4 bits */ +#define CHRG_CCCV_CC_BIT_POS 0 +#define CHRG_CCCV_CC_OFFSET 200 /* 200mA */ +#define CHRG_CCCV_CC_LSB_RES 200 /* 200mA */ +#define CHRG_CCCV_ITERM_20P BIT(4) /* 20% of CC */ +#define CHRG_CCCV_CV_MASK 0x60 /* 2 bits */ +#define CHRG_CCCV_CV_BIT_POS 5 +#define CHRG_CCCV_CV_4100MV 0x0 /* 4.10V */ +#define CHRG_CCCV_CV_4150MV 0x1 /* 4.15V */ +#define CHRG_CCCV_CV_4200MV 0x2 /* 4.20V */ +#define CHRG_CCCV_CV_4350MV 0x3 /* 4.35V */ +#define CHRG_CCCV_CHG_EN BIT(7) + +#define CNTL2_CC_TIMEOUT_MASK 0x3 /* 2 bits */ +#define CNTL2_CC_TIMEOUT_OFFSET 6 /* 6 Hrs */ +#define CNTL2_CC_TIMEOUT_LSB_RES 2 /* 2 Hrs */ +#define CNTL2_CC_TIMEOUT_12HRS 0x3 /* 12 Hrs */ +#define CNTL2_CHGLED_TYPEB BIT(4) +#define CNTL2_CHG_OUT_TURNON BIT(5) +#define CNTL2_PC_TIMEOUT_MASK 0xC0 +#define CNTL2_PC_TIMEOUT_OFFSET 40 /* 40 mins */ +#define CNTL2_PC_TIMEOUT_LSB_RES 10 /* 10 mins */ +#define CNTL2_PC_TIMEOUT_70MINS 0x3 + +#define CHRG_ILIM_TEMP_LOOP_EN BIT(3) +#define CHRG_VBUS_ILIM_MASK 0xf0 +#define CHRG_VBUS_ILIM_BIT_POS 4 +#define CHRG_VBUS_ILIM_100MA 0x0 /* 100mA */ +#define CHRG_VBUS_ILIM_500MA 0x1 /* 500mA */ +#define CHRG_VBUS_ILIM_900MA 0x2 /* 900mA */ +#define CHRG_VBUS_ILIM_1500MA 0x3 /* 1500mA */ +#define CHRG_VBUS_ILIM_2000MA 0x4 /* 2000mA */ +#define CHRG_VBUS_ILIM_2500MA 0x5 /* 2500mA */ +#define CHRG_VBUS_ILIM_3000MA 0x6 /* 3000mA */ +#define CHRG_VBUS_ILIM_3500MA 0x7 /* 3500mA */ +#define CHRG_VBUS_ILIM_4000MA 0x8 /* 4000mA */ + +#define CHRG_VLTFC_0C 0xA5 /* 0 DegC */ +#define CHRG_VHTFC_45C 0x1F /* 45 DegC */ + +#define FG_CNTL_OCV_ADJ_EN BIT(3) + +#define CV_4100MV 4100 /* 4100mV */ +#define CV_4150MV 4150 /* 4150mV */ +#define CV_4200MV 4200 /* 4200mV */ +#define CV_4350MV 4350 /* 4350mV */ + +#define AXP288_EXTCON_DEV_NAME "axp288_extcon" +#define USB_HOST_EXTCON_HID "INT3496" +#define USB_HOST_EXTCON_NAME "INT3496:00" + +enum { + VBUS_OV_IRQ = 0, + CHARGE_DONE_IRQ, + CHARGE_CHARGING_IRQ, + BAT_SAFE_QUIT_IRQ, + BAT_SAFE_ENTER_IRQ, + QCBTU_IRQ, + CBTU_IRQ, + QCBTO_IRQ, + CBTO_IRQ, + CHRG_INTR_END, +}; + +struct axp288_chrg_info { + struct platform_device *pdev; + struct regmap *regmap; + struct regmap_irq_chip_data *regmap_irqc; + int irq[CHRG_INTR_END]; + struct power_supply *psy_usb; + + /* OTG/Host mode */ + struct { + struct work_struct work; + struct extcon_dev *cable; + struct notifier_block id_nb; + bool id_short; + } otg; + + /* SDP/CDP/DCP USB charging cable notifications */ + struct { + struct extcon_dev *edev; + struct notifier_block nb; + struct work_struct work; + } cable; + + int cc; + int cv; + int max_cc; + int max_cv; +}; + +static inline int axp288_charger_set_cc(struct axp288_chrg_info *info, int cc) +{ + u8 reg_val; + int ret; + + if (cc < CHRG_CCCV_CC_OFFSET) + cc = CHRG_CCCV_CC_OFFSET; + else if (cc > info->max_cc) + cc = info->max_cc; + + reg_val = (cc - CHRG_CCCV_CC_OFFSET) / CHRG_CCCV_CC_LSB_RES; + cc = (reg_val * CHRG_CCCV_CC_LSB_RES) + CHRG_CCCV_CC_OFFSET; + reg_val = reg_val << CHRG_CCCV_CC_BIT_POS; + + ret = regmap_update_bits(info->regmap, + AXP20X_CHRG_CTRL1, + CHRG_CCCV_CC_MASK, reg_val); + if (ret >= 0) + info->cc = cc; + + return ret; +} + +static inline int axp288_charger_set_cv(struct axp288_chrg_info *info, int cv) +{ + u8 reg_val; + int ret; + + if (cv <= CV_4100MV) { + reg_val = CHRG_CCCV_CV_4100MV; + cv = CV_4100MV; + } else if (cv <= CV_4150MV) { + reg_val = CHRG_CCCV_CV_4150MV; + cv = CV_4150MV; + } else if (cv <= CV_4200MV) { + reg_val = CHRG_CCCV_CV_4200MV; + cv = CV_4200MV; + } else { + reg_val = CHRG_CCCV_CV_4350MV; + cv = CV_4350MV; + } + + reg_val = reg_val << CHRG_CCCV_CV_BIT_POS; + + ret = regmap_update_bits(info->regmap, + AXP20X_CHRG_CTRL1, + CHRG_CCCV_CV_MASK, reg_val); + + if (ret >= 0) + info->cv = cv; + + return ret; +} + +static int axp288_charger_get_vbus_inlmt(struct axp288_chrg_info *info) +{ + unsigned int val; + int ret; + + ret = regmap_read(info->regmap, AXP20X_CHRG_BAK_CTRL, &val); + if (ret < 0) + return ret; + + val >>= CHRG_VBUS_ILIM_BIT_POS; + switch (val) { + case CHRG_VBUS_ILIM_100MA: + return 100000; + case CHRG_VBUS_ILIM_500MA: + return 500000; + case CHRG_VBUS_ILIM_900MA: + return 900000; + case CHRG_VBUS_ILIM_1500MA: + return 1500000; + case CHRG_VBUS_ILIM_2000MA: + return 2000000; + case CHRG_VBUS_ILIM_2500MA: + return 2500000; + case CHRG_VBUS_ILIM_3000MA: + return 3000000; + case CHRG_VBUS_ILIM_3500MA: + return 3500000; + default: + /* All b1xxx values map to 4000 mA */ + return 4000000; + } +} + +static inline int axp288_charger_set_vbus_inlmt(struct axp288_chrg_info *info, + int inlmt) +{ + int ret; + u8 reg_val; + + if (inlmt >= 4000000) + reg_val = CHRG_VBUS_ILIM_4000MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 3500000) + reg_val = CHRG_VBUS_ILIM_3500MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 3000000) + reg_val = CHRG_VBUS_ILIM_3000MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 2500000) + reg_val = CHRG_VBUS_ILIM_2500MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 2000000) + reg_val = CHRG_VBUS_ILIM_2000MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 1500000) + reg_val = CHRG_VBUS_ILIM_1500MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 900000) + reg_val = CHRG_VBUS_ILIM_900MA << CHRG_VBUS_ILIM_BIT_POS; + else if (inlmt >= 500000) + reg_val = CHRG_VBUS_ILIM_500MA << CHRG_VBUS_ILIM_BIT_POS; + else + reg_val = CHRG_VBUS_ILIM_100MA << CHRG_VBUS_ILIM_BIT_POS; + + ret = regmap_update_bits(info->regmap, AXP20X_CHRG_BAK_CTRL, + CHRG_VBUS_ILIM_MASK, reg_val); + if (ret < 0) + dev_err(&info->pdev->dev, "charger BAK control %d\n", ret); + + return ret; +} + +static int axp288_charger_vbus_path_select(struct axp288_chrg_info *info, + bool enable) +{ + int ret; + + if (enable) + ret = regmap_update_bits(info->regmap, AXP20X_VBUS_IPSOUT_MGMT, + VBUS_ISPOUT_VBUS_PATH_DIS, 0); + else + ret = regmap_update_bits(info->regmap, AXP20X_VBUS_IPSOUT_MGMT, + VBUS_ISPOUT_VBUS_PATH_DIS, VBUS_ISPOUT_VBUS_PATH_DIS); + + if (ret < 0) + dev_err(&info->pdev->dev, "axp288 vbus path select %d\n", ret); + + return ret; +} + +static int axp288_charger_enable_charger(struct axp288_chrg_info *info, + bool enable) +{ + int ret; + + if (enable) + ret = regmap_update_bits(info->regmap, AXP20X_CHRG_CTRL1, + CHRG_CCCV_CHG_EN, CHRG_CCCV_CHG_EN); + else + ret = regmap_update_bits(info->regmap, AXP20X_CHRG_CTRL1, + CHRG_CCCV_CHG_EN, 0); + if (ret < 0) + dev_err(&info->pdev->dev, "axp288 enable charger %d\n", ret); + + return ret; +} + +static int axp288_charger_is_present(struct axp288_chrg_info *info) +{ + int ret, present = 0; + unsigned int val; + + ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); + if (ret < 0) + return ret; + + if (val & PS_STAT_VBUS_PRESENT) + present = 1; + return present; +} + +static int axp288_charger_is_online(struct axp288_chrg_info *info) +{ + int ret, online = 0; + unsigned int val; + + ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); + if (ret < 0) + return ret; + + if (val & PS_STAT_VBUS_VALID) + online = 1; + return online; +} + +static int axp288_get_charger_health(struct axp288_chrg_info *info) +{ + int ret, pwr_stat, chrg_stat; + int health = POWER_SUPPLY_HEALTH_UNKNOWN; + unsigned int val; + + ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); + if ((ret < 0) || !(val & PS_STAT_VBUS_PRESENT)) + goto health_read_fail; + else + pwr_stat = val; + + ret = regmap_read(info->regmap, AXP20X_PWR_OP_MODE, &val); + if (ret < 0) + goto health_read_fail; + else + chrg_stat = val; + + if (!(pwr_stat & PS_STAT_VBUS_VALID)) + health = POWER_SUPPLY_HEALTH_DEAD; + else if (chrg_stat & CHRG_STAT_PMIC_OTP) + health = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (chrg_stat & CHRG_STAT_BAT_SAFE_MODE) + health = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + else + health = POWER_SUPPLY_HEALTH_GOOD; + +health_read_fail: + return health; +} + +static int axp288_charger_usb_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct axp288_chrg_info *info = power_supply_get_drvdata(psy); + int ret = 0; + int scaled_val; + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + scaled_val = min(val->intval, info->max_cc); + scaled_val = DIV_ROUND_CLOSEST(scaled_val, 1000); + ret = axp288_charger_set_cc(info, scaled_val); + if (ret < 0) + dev_warn(&info->pdev->dev, "set charge current failed\n"); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + scaled_val = min(val->intval, info->max_cv); + scaled_val = DIV_ROUND_CLOSEST(scaled_val, 1000); + ret = axp288_charger_set_cv(info, scaled_val); + if (ret < 0) + dev_warn(&info->pdev->dev, "set charge voltage failed\n"); + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = axp288_charger_set_vbus_inlmt(info, val->intval); + if (ret < 0) + dev_warn(&info->pdev->dev, "set input current limit failed\n"); + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static int axp288_charger_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct axp288_chrg_info *info = power_supply_get_drvdata(psy); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + /* Check for OTG case first */ + if (info->otg.id_short) { + val->intval = 0; + break; + } + ret = axp288_charger_is_present(info); + if (ret < 0) + return ret; + val->intval = ret; + break; + case POWER_SUPPLY_PROP_ONLINE: + /* Check for OTG case first */ + if (info->otg.id_short) { + val->intval = 0; + break; + } + ret = axp288_charger_is_online(info); + if (ret < 0) + return ret; + val->intval = ret; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = axp288_get_charger_health(info); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + val->intval = info->cc * 1000; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = info->max_cc * 1000; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + val->intval = info->cv * 1000; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + val->intval = info->max_cv * 1000; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = axp288_charger_get_vbus_inlmt(info); + if (ret < 0) + return ret; + val->intval = ret; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int axp288_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} + +static enum power_supply_property axp288_usb_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_TYPE, + POWER_SUPPLY_PROP_HEALTH, + 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, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, +}; + +static const struct power_supply_desc axp288_charger_desc = { + .name = "axp288_charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = axp288_usb_props, + .num_properties = ARRAY_SIZE(axp288_usb_props), + .get_property = axp288_charger_usb_get_property, + .set_property = axp288_charger_usb_set_property, + .property_is_writeable = axp288_charger_property_is_writeable, +}; + +static irqreturn_t axp288_charger_irq_thread_handler(int irq, void *dev) +{ + struct axp288_chrg_info *info = dev; + int i; + + for (i = 0; i < CHRG_INTR_END; i++) { + if (info->irq[i] == irq) + break; + } + + if (i >= CHRG_INTR_END) { + dev_warn(&info->pdev->dev, "spurious interrupt!!\n"); + return IRQ_NONE; + } + + switch (i) { + case VBUS_OV_IRQ: + dev_dbg(&info->pdev->dev, "VBUS Over Voltage INTR\n"); + break; + case CHARGE_DONE_IRQ: + dev_dbg(&info->pdev->dev, "Charging Done INTR\n"); + break; + case CHARGE_CHARGING_IRQ: + dev_dbg(&info->pdev->dev, "Start Charging IRQ\n"); + break; + case BAT_SAFE_QUIT_IRQ: + dev_dbg(&info->pdev->dev, + "Quit Safe Mode(restart timer) Charging IRQ\n"); + break; + case BAT_SAFE_ENTER_IRQ: + dev_dbg(&info->pdev->dev, + "Enter Safe Mode(timer expire) Charging IRQ\n"); + break; + case QCBTU_IRQ: + dev_dbg(&info->pdev->dev, + "Quit Battery Under Temperature(CHRG) INTR\n"); + break; + case CBTU_IRQ: + dev_dbg(&info->pdev->dev, + "Hit Battery Under Temperature(CHRG) INTR\n"); + break; + case QCBTO_IRQ: + dev_dbg(&info->pdev->dev, + "Quit Battery Over Temperature(CHRG) INTR\n"); + break; + case CBTO_IRQ: + dev_dbg(&info->pdev->dev, + "Hit Battery Over Temperature(CHRG) INTR\n"); + break; + default: + dev_warn(&info->pdev->dev, "Spurious Interrupt!!!\n"); + goto out; + } + + power_supply_changed(info->psy_usb); +out: + return IRQ_HANDLED; +} + +/* + * The HP Pavilion x2 10 series comes in a number of variants: + * Bay Trail SoC + AXP288 PMIC, Micro-USB, DMI_BOARD_NAME: "8021" + * Bay Trail SoC + AXP288 PMIC, Type-C, DMI_BOARD_NAME: "815D" + * Cherry Trail SoC + AXP288 PMIC, Type-C, DMI_BOARD_NAME: "813E" + * Cherry Trail SoC + TI PMIC, Type-C, DMI_BOARD_NAME: "827C" or "82F4" + * + * The variants with the AXP288 + Type-C connector are all kinds of special: + * + * 1. They use a Type-C connector which the AXP288 does not support, so when + * using a Type-C charger it is not recognized. Unlike most AXP288 devices, + * this model actually has mostly working ACPI AC / Battery code, the ACPI code + * "solves" this by simply setting the input_current_limit to 3A. + * There are still some issues with the ACPI code, so we use this native driver, + * and to solve the charging not working (500mA is not enough) issue we hardcode + * the 3A input_current_limit like the ACPI code does. + * + * 2. If no charger is connected the machine boots with the vbus-path disabled. + * Normally this is done when a 5V boost converter is active to avoid the PMIC + * trying to charge from the 5V boost converter's output. This is done when + * an OTG host cable is inserted and the ID pin on the micro-B receptacle is + * pulled low and the ID pin has an ACPI event handler associated with it + * which re-enables the vbus-path when the ID pin is pulled high when the + * OTG host cable is removed. The Type-C connector has no ID pin, there is + * no ID pin handler and there appears to be no 5V boost converter, so we + * end up not charging because the vbus-path is disabled, until we unplug + * the charger which automatically clears the vbus-path disable bit and then + * on the second plug-in of the adapter we start charging. To solve the not + * charging on first charger plugin we unconditionally enable the vbus-path at + * probe on this model, which is safe since there is no 5V boost converter. + */ +static const struct dmi_system_id axp288_hp_x2_dmi_ids[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Hewlett-Packard"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "HP Pavilion x2 Detachable"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "815D"), + }, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "HP Pavilion x2 Detachable"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "813E"), + }, + }, + {} /* Terminating entry */ +}; + +static void axp288_charger_extcon_evt_worker(struct work_struct *work) +{ + struct axp288_chrg_info *info = + container_of(work, struct axp288_chrg_info, cable.work); + int ret, current_limit; + struct extcon_dev *edev = info->cable.edev; + unsigned int val; + + ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); + if (ret < 0) { + dev_err(&info->pdev->dev, "Error reading status (%d)\n", ret); + return; + } + + /* Offline? Disable charging and bail */ + if (!(val & PS_STAT_VBUS_VALID)) { + dev_dbg(&info->pdev->dev, "USB charger disconnected\n"); + axp288_charger_enable_charger(info, false); + power_supply_changed(info->psy_usb); + return; + } + + /* Determine cable/charger type */ + if (dmi_check_system(axp288_hp_x2_dmi_ids)) { + /* See comment above axp288_hp_x2_dmi_ids declaration */ + dev_dbg(&info->pdev->dev, "HP X2 with Type-C, setting inlmt to 3A\n"); + current_limit = 3000000; + } else if (extcon_get_state(edev, EXTCON_CHG_USB_SDP) > 0) { + dev_dbg(&info->pdev->dev, "USB SDP charger is connected\n"); + current_limit = 500000; + } else if (extcon_get_state(edev, EXTCON_CHG_USB_CDP) > 0) { + dev_dbg(&info->pdev->dev, "USB CDP charger is connected\n"); + current_limit = 1500000; + } else if (extcon_get_state(edev, EXTCON_CHG_USB_DCP) > 0) { + dev_dbg(&info->pdev->dev, "USB DCP charger is connected\n"); + current_limit = 2000000; + } else { + /* Charger type detection still in progress, bail. */ + return; + } + + /* Set vbus current limit first, then enable charger */ + ret = axp288_charger_set_vbus_inlmt(info, current_limit); + if (ret == 0) + axp288_charger_enable_charger(info, true); + else + dev_err(&info->pdev->dev, + "error setting current limit (%d)\n", ret); + + power_supply_changed(info->psy_usb); +} + +static int axp288_charger_handle_cable_evt(struct notifier_block *nb, + unsigned long event, void *param) +{ + struct axp288_chrg_info *info = + container_of(nb, struct axp288_chrg_info, cable.nb); + schedule_work(&info->cable.work); + return NOTIFY_OK; +} + +static void axp288_charger_otg_evt_worker(struct work_struct *work) +{ + struct axp288_chrg_info *info = + container_of(work, struct axp288_chrg_info, otg.work); + struct extcon_dev *edev = info->otg.cable; + int ret, usb_host = extcon_get_state(edev, EXTCON_USB_HOST); + + dev_dbg(&info->pdev->dev, "external connector USB-Host is %s\n", + usb_host ? "attached" : "detached"); + + /* + * Set usb_id_short flag to avoid running charger detection logic + * in case usb host. + */ + info->otg.id_short = usb_host; + + /* Disable VBUS path before enabling the 5V boost */ + ret = axp288_charger_vbus_path_select(info, !info->otg.id_short); + if (ret < 0) + dev_warn(&info->pdev->dev, "vbus path disable failed\n"); +} + +static int axp288_charger_handle_otg_evt(struct notifier_block *nb, + unsigned long event, void *param) +{ + struct axp288_chrg_info *info = + container_of(nb, struct axp288_chrg_info, otg.id_nb); + + schedule_work(&info->otg.work); + + return NOTIFY_OK; +} + +static int charger_init_hw_regs(struct axp288_chrg_info *info) +{ + int ret, cc, cv; + unsigned int val; + + /* Program temperature thresholds */ + ret = regmap_write(info->regmap, AXP20X_V_LTF_CHRG, CHRG_VLTFC_0C); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_V_LTF_CHRG, ret); + return ret; + } + + ret = regmap_write(info->regmap, AXP20X_V_HTF_CHRG, CHRG_VHTFC_45C); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_V_HTF_CHRG, ret); + return ret; + } + + /* Do not turn-off charger o/p after charge cycle ends */ + ret = regmap_update_bits(info->regmap, + AXP20X_CHRG_CTRL2, + CNTL2_CHG_OUT_TURNON, CNTL2_CHG_OUT_TURNON); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_CHRG_CTRL2, ret); + return ret; + } + + /* Setup ending condition for charging to be 10% of I(chrg) */ + ret = regmap_update_bits(info->regmap, + AXP20X_CHRG_CTRL1, + CHRG_CCCV_ITERM_20P, 0); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_CHRG_CTRL1, ret); + return ret; + } + + /* Disable OCV-SOC curve calibration */ + ret = regmap_update_bits(info->regmap, + AXP20X_CC_CTRL, + FG_CNTL_OCV_ADJ_EN, 0); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_CC_CTRL, ret); + return ret; + } + + if (dmi_check_system(axp288_hp_x2_dmi_ids)) { + /* See comment above axp288_hp_x2_dmi_ids declaration */ + ret = axp288_charger_vbus_path_select(info, true); + if (ret < 0) + return ret; + } else { + /* Set Vhold to the factory default / recommended 4.4V */ + val = VBUS_ISPOUT_VHOLD_SET_4400MV << VBUS_ISPOUT_VHOLD_SET_BIT_POS; + ret = regmap_update_bits(info->regmap, AXP20X_VBUS_IPSOUT_MGMT, + VBUS_ISPOUT_VHOLD_SET_MASK, val); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_VBUS_IPSOUT_MGMT, ret); + return ret; + } + } + + /* Read current charge voltage and current limit */ + ret = regmap_read(info->regmap, AXP20X_CHRG_CTRL1, &val); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) read error(%d)\n", + AXP20X_CHRG_CTRL1, ret); + return ret; + } + + /* Determine charge voltage */ + cv = (val & CHRG_CCCV_CV_MASK) >> CHRG_CCCV_CV_BIT_POS; + switch (cv) { + case CHRG_CCCV_CV_4100MV: + info->cv = CV_4100MV; + break; + case CHRG_CCCV_CV_4150MV: + info->cv = CV_4150MV; + break; + case CHRG_CCCV_CV_4200MV: + info->cv = CV_4200MV; + break; + case CHRG_CCCV_CV_4350MV: + info->cv = CV_4350MV; + break; + } + + /* Determine charge current limit */ + cc = (val & CHRG_CCCV_CC_MASK) >> CHRG_CCCV_CC_BIT_POS; + cc = (cc * CHRG_CCCV_CC_LSB_RES) + CHRG_CCCV_CC_OFFSET; + info->cc = cc; + + /* + * Do not allow the user to configure higher settings then those + * set by the firmware + */ + info->max_cv = info->cv; + info->max_cc = info->cc; + + return 0; +} + +static void axp288_charger_cancel_work(void *data) +{ + struct axp288_chrg_info *info = data; + + cancel_work_sync(&info->otg.work); + cancel_work_sync(&info->cable.work); +} + +static int axp288_charger_probe(struct platform_device *pdev) +{ + int ret, i, pirq; + struct axp288_chrg_info *info; + struct device *dev = &pdev->dev; + struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config charger_cfg = {}; + unsigned int val; + + /* + * On some devices the fuelgauge and charger parts of the axp288 are + * not used, check that the fuelgauge is enabled (CC_CTRL != 0). + */ + ret = regmap_read(axp20x->regmap, AXP20X_CC_CTRL, &val); + if (ret < 0) + return ret; + if (val == 0) + return -ENODEV; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->pdev = pdev; + info->regmap = axp20x->regmap; + info->regmap_irqc = axp20x->regmap_irqc; + + info->cable.edev = extcon_get_extcon_dev(AXP288_EXTCON_DEV_NAME); + if (info->cable.edev == NULL) { + dev_dbg(&pdev->dev, "%s is not ready, probe deferred\n", + AXP288_EXTCON_DEV_NAME); + return -EPROBE_DEFER; + } + + if (acpi_dev_present(USB_HOST_EXTCON_HID, NULL, -1)) { + info->otg.cable = extcon_get_extcon_dev(USB_HOST_EXTCON_NAME); + if (info->otg.cable == NULL) { + dev_dbg(dev, "EXTCON_USB_HOST is not ready, probe deferred\n"); + return -EPROBE_DEFER; + } + dev_info(&pdev->dev, + "Using " USB_HOST_EXTCON_HID " extcon for usb-id\n"); + } + + platform_set_drvdata(pdev, info); + + ret = charger_init_hw_regs(info); + if (ret) + return ret; + + /* Register with power supply class */ + charger_cfg.drv_data = info; + info->psy_usb = devm_power_supply_register(dev, &axp288_charger_desc, + &charger_cfg); + if (IS_ERR(info->psy_usb)) { + ret = PTR_ERR(info->psy_usb); + dev_err(dev, "failed to register power supply: %d\n", ret); + return ret; + } + + /* Cancel our work on cleanup, register this before the notifiers */ + ret = devm_add_action(dev, axp288_charger_cancel_work, info); + if (ret) + return ret; + + /* Register for extcon notification */ + INIT_WORK(&info->cable.work, axp288_charger_extcon_evt_worker); + info->cable.nb.notifier_call = axp288_charger_handle_cable_evt; + ret = devm_extcon_register_notifier_all(dev, info->cable.edev, + &info->cable.nb); + if (ret) { + dev_err(dev, "failed to register cable extcon notifier\n"); + return ret; + } + schedule_work(&info->cable.work); + + /* Register for OTG notification */ + INIT_WORK(&info->otg.work, axp288_charger_otg_evt_worker); + info->otg.id_nb.notifier_call = axp288_charger_handle_otg_evt; + if (info->otg.cable) { + ret = devm_extcon_register_notifier(&pdev->dev, info->otg.cable, + EXTCON_USB_HOST, &info->otg.id_nb); + if (ret) { + dev_err(dev, "failed to register EXTCON_USB_HOST notifier\n"); + return ret; + } + schedule_work(&info->otg.work); + } + + /* Register charger interrupts */ + for (i = 0; i < CHRG_INTR_END; i++) { + pirq = platform_get_irq(info->pdev, i); + if (pirq < 0) + return pirq; + + info->irq[i] = regmap_irq_get_virq(info->regmap_irqc, pirq); + if (info->irq[i] < 0) { + dev_warn(&info->pdev->dev, + "failed to get virtual interrupt=%d\n", pirq); + return info->irq[i]; + } + ret = devm_request_threaded_irq(&info->pdev->dev, info->irq[i], + NULL, axp288_charger_irq_thread_handler, + IRQF_ONESHOT, info->pdev->name, info); + if (ret) { + dev_err(&pdev->dev, "failed to request interrupt=%d\n", + info->irq[i]); + return ret; + } + } + + return 0; +} + +static const struct platform_device_id axp288_charger_id_table[] = { + { .name = "axp288_charger" }, + {}, +}; +MODULE_DEVICE_TABLE(platform, axp288_charger_id_table); + +static struct platform_driver axp288_charger_driver = { + .probe = axp288_charger_probe, + .id_table = axp288_charger_id_table, + .driver = { + .name = "axp288_charger", + }, +}; + +module_platform_driver(axp288_charger_driver); + +MODULE_AUTHOR("Ramakrishna Pallala <ramakrishna.pallala@intel.com>"); +MODULE_DESCRIPTION("X-power AXP288 Charger Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/axp288_fuel_gauge.c b/drivers/power/supply/axp288_fuel_gauge.c new file mode 100644 index 000000000..be2452915 --- /dev/null +++ b/drivers/power/supply/axp288_fuel_gauge.c @@ -0,0 +1,894 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * axp288_fuel_gauge.c - Xpower AXP288 PMIC Fuel Gauge Driver + * + * Copyright (C) 2016-2017 Hans de Goede <hdegoede@redhat.com> + * Copyright (C) 2014 Intel Corporation + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/dmi.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/device.h> +#include <linux/regmap.h> +#include <linux/jiffies.h> +#include <linux/interrupt.h> +#include <linux/mfd/axp20x.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/iio/consumer.h> +#include <linux/debugfs.h> +#include <linux/seq_file.h> +#include <asm/unaligned.h> + +#define PS_STAT_VBUS_TRIGGER (1 << 0) +#define PS_STAT_BAT_CHRG_DIR (1 << 2) +#define PS_STAT_VBAT_ABOVE_VHOLD (1 << 3) +#define PS_STAT_VBUS_VALID (1 << 4) +#define PS_STAT_VBUS_PRESENT (1 << 5) + +#define CHRG_STAT_BAT_SAFE_MODE (1 << 3) +#define CHRG_STAT_BAT_VALID (1 << 4) +#define CHRG_STAT_BAT_PRESENT (1 << 5) +#define CHRG_STAT_CHARGING (1 << 6) +#define CHRG_STAT_PMIC_OTP (1 << 7) + +#define CHRG_CCCV_CC_MASK 0xf /* 4 bits */ +#define CHRG_CCCV_CC_BIT_POS 0 +#define CHRG_CCCV_CC_OFFSET 200 /* 200mA */ +#define CHRG_CCCV_CC_LSB_RES 200 /* 200mA */ +#define CHRG_CCCV_ITERM_20P (1 << 4) /* 20% of CC */ +#define CHRG_CCCV_CV_MASK 0x60 /* 2 bits */ +#define CHRG_CCCV_CV_BIT_POS 5 +#define CHRG_CCCV_CV_4100MV 0x0 /* 4.10V */ +#define CHRG_CCCV_CV_4150MV 0x1 /* 4.15V */ +#define CHRG_CCCV_CV_4200MV 0x2 /* 4.20V */ +#define CHRG_CCCV_CV_4350MV 0x3 /* 4.35V */ +#define CHRG_CCCV_CHG_EN (1 << 7) + +#define FG_CNTL_OCV_ADJ_STAT (1 << 2) +#define FG_CNTL_OCV_ADJ_EN (1 << 3) +#define FG_CNTL_CAP_ADJ_STAT (1 << 4) +#define FG_CNTL_CAP_ADJ_EN (1 << 5) +#define FG_CNTL_CC_EN (1 << 6) +#define FG_CNTL_GAUGE_EN (1 << 7) + +#define FG_15BIT_WORD_VALID (1 << 15) +#define FG_15BIT_VAL_MASK 0x7fff + +#define FG_REP_CAP_VALID (1 << 7) +#define FG_REP_CAP_VAL_MASK 0x7F + +#define FG_DES_CAP1_VALID (1 << 7) +#define FG_DES_CAP_RES_LSB 1456 /* 1.456mAhr */ + +#define FG_DES_CC_RES_LSB 1456 /* 1.456mAhr */ + +#define FG_OCV_CAP_VALID (1 << 7) +#define FG_OCV_CAP_VAL_MASK 0x7F +#define FG_CC_CAP_VALID (1 << 7) +#define FG_CC_CAP_VAL_MASK 0x7F + +#define FG_LOW_CAP_THR1_MASK 0xf0 /* 5% tp 20% */ +#define FG_LOW_CAP_THR1_VAL 0xa0 /* 15 perc */ +#define FG_LOW_CAP_THR2_MASK 0x0f /* 0% to 15% */ +#define FG_LOW_CAP_WARN_THR 14 /* 14 perc */ +#define FG_LOW_CAP_CRIT_THR 4 /* 4 perc */ +#define FG_LOW_CAP_SHDN_THR 0 /* 0 perc */ + +#define NR_RETRY_CNT 3 +#define DEV_NAME "axp288_fuel_gauge" + +/* 1.1mV per LSB expressed in uV */ +#define VOLTAGE_FROM_ADC(a) ((a * 11) / 10) +/* properties converted to uV, uA */ +#define PROP_VOLT(a) ((a) * 1000) +#define PROP_CURR(a) ((a) * 1000) + +#define AXP288_FG_INTR_NUM 6 +enum { + QWBTU_IRQ = 0, + WBTU_IRQ, + QWBTO_IRQ, + WBTO_IRQ, + WL2_IRQ, + WL1_IRQ, +}; + +enum { + BAT_TEMP = 0, + PMIC_TEMP, + SYSTEM_TEMP, + BAT_CHRG_CURR, + BAT_D_CURR, + BAT_VOLT, + IIO_CHANNEL_NUM +}; + +struct axp288_fg_info { + struct platform_device *pdev; + struct regmap *regmap; + struct regmap_irq_chip_data *regmap_irqc; + int irq[AXP288_FG_INTR_NUM]; + struct iio_channel *iio_channel[IIO_CHANNEL_NUM]; + struct power_supply *bat; + struct mutex lock; + int status; + int max_volt; + struct dentry *debug_file; +}; + +static enum power_supply_property fuel_gauge_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + 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_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, +}; + +static int fuel_gauge_reg_readb(struct axp288_fg_info *info, int reg) +{ + int ret, i; + unsigned int val; + + for (i = 0; i < NR_RETRY_CNT; i++) { + ret = regmap_read(info->regmap, reg, &val); + if (ret == -EBUSY) + continue; + else + break; + } + + if (ret < 0) { + dev_err(&info->pdev->dev, "Error reading reg 0x%02x err: %d\n", reg, ret); + return ret; + } + + return val; +} + +static int fuel_gauge_reg_writeb(struct axp288_fg_info *info, int reg, u8 val) +{ + int ret; + + ret = regmap_write(info->regmap, reg, (unsigned int)val); + + if (ret < 0) + dev_err(&info->pdev->dev, "Error writing reg 0x%02x err: %d\n", reg, ret); + + return ret; +} + +static int fuel_gauge_read_15bit_word(struct axp288_fg_info *info, int reg) +{ + unsigned char buf[2]; + int ret; + + ret = regmap_bulk_read(info->regmap, reg, buf, 2); + if (ret < 0) { + dev_err(&info->pdev->dev, "Error reading reg 0x%02x err: %d\n", + reg, ret); + return ret; + } + + ret = get_unaligned_be16(buf); + if (!(ret & FG_15BIT_WORD_VALID)) { + dev_err(&info->pdev->dev, "Error reg 0x%02x contents not valid\n", + reg); + return -ENXIO; + } + + return ret & FG_15BIT_VAL_MASK; +} + +static int fuel_gauge_read_12bit_word(struct axp288_fg_info *info, int reg) +{ + unsigned char buf[2]; + int ret; + + ret = regmap_bulk_read(info->regmap, reg, buf, 2); + if (ret < 0) { + dev_err(&info->pdev->dev, "Error reading reg 0x%02x err: %d\n", + reg, ret); + return ret; + } + + /* 12-bit data values have upper 8 bits in buf[0], lower 4 in buf[1] */ + return (buf[0] << 4) | ((buf[1] >> 4) & 0x0f); +} + +#ifdef CONFIG_DEBUG_FS +static int fuel_gauge_debug_show(struct seq_file *s, void *data) +{ + struct axp288_fg_info *info = s->private; + int raw_val, ret; + + seq_printf(s, " PWR_STATUS[%02x] : %02x\n", + AXP20X_PWR_INPUT_STATUS, + fuel_gauge_reg_readb(info, AXP20X_PWR_INPUT_STATUS)); + seq_printf(s, "PWR_OP_MODE[%02x] : %02x\n", + AXP20X_PWR_OP_MODE, + fuel_gauge_reg_readb(info, AXP20X_PWR_OP_MODE)); + seq_printf(s, " CHRG_CTRL1[%02x] : %02x\n", + AXP20X_CHRG_CTRL1, + fuel_gauge_reg_readb(info, AXP20X_CHRG_CTRL1)); + seq_printf(s, " VLTF[%02x] : %02x\n", + AXP20X_V_LTF_DISCHRG, + fuel_gauge_reg_readb(info, AXP20X_V_LTF_DISCHRG)); + seq_printf(s, " VHTF[%02x] : %02x\n", + AXP20X_V_HTF_DISCHRG, + fuel_gauge_reg_readb(info, AXP20X_V_HTF_DISCHRG)); + seq_printf(s, " CC_CTRL[%02x] : %02x\n", + AXP20X_CC_CTRL, + fuel_gauge_reg_readb(info, AXP20X_CC_CTRL)); + seq_printf(s, "BATTERY CAP[%02x] : %02x\n", + AXP20X_FG_RES, + fuel_gauge_reg_readb(info, AXP20X_FG_RES)); + seq_printf(s, " FG_RDC1[%02x] : %02x\n", + AXP288_FG_RDC1_REG, + fuel_gauge_reg_readb(info, AXP288_FG_RDC1_REG)); + seq_printf(s, " FG_RDC0[%02x] : %02x\n", + AXP288_FG_RDC0_REG, + fuel_gauge_reg_readb(info, AXP288_FG_RDC0_REG)); + seq_printf(s, " FG_OCV[%02x] : %04x\n", + AXP288_FG_OCVH_REG, + fuel_gauge_read_12bit_word(info, AXP288_FG_OCVH_REG)); + seq_printf(s, " FG_DES_CAP[%02x] : %04x\n", + AXP288_FG_DES_CAP1_REG, + fuel_gauge_read_15bit_word(info, AXP288_FG_DES_CAP1_REG)); + seq_printf(s, " FG_CC_MTR[%02x] : %04x\n", + AXP288_FG_CC_MTR1_REG, + fuel_gauge_read_15bit_word(info, AXP288_FG_CC_MTR1_REG)); + seq_printf(s, " FG_OCV_CAP[%02x] : %02x\n", + AXP288_FG_OCV_CAP_REG, + fuel_gauge_reg_readb(info, AXP288_FG_OCV_CAP_REG)); + seq_printf(s, " FG_CC_CAP[%02x] : %02x\n", + AXP288_FG_CC_CAP_REG, + fuel_gauge_reg_readb(info, AXP288_FG_CC_CAP_REG)); + seq_printf(s, " FG_LOW_CAP[%02x] : %02x\n", + AXP288_FG_LOW_CAP_REG, + fuel_gauge_reg_readb(info, AXP288_FG_LOW_CAP_REG)); + seq_printf(s, "TUNING_CTL0[%02x] : %02x\n", + AXP288_FG_TUNE0, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE0)); + seq_printf(s, "TUNING_CTL1[%02x] : %02x\n", + AXP288_FG_TUNE1, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE1)); + seq_printf(s, "TUNING_CTL2[%02x] : %02x\n", + AXP288_FG_TUNE2, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE2)); + seq_printf(s, "TUNING_CTL3[%02x] : %02x\n", + AXP288_FG_TUNE3, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE3)); + seq_printf(s, "TUNING_CTL4[%02x] : %02x\n", + AXP288_FG_TUNE4, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE4)); + seq_printf(s, "TUNING_CTL5[%02x] : %02x\n", + AXP288_FG_TUNE5, + fuel_gauge_reg_readb(info, AXP288_FG_TUNE5)); + + ret = iio_read_channel_raw(info->iio_channel[BAT_TEMP], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-batttemp : %d\n", raw_val); + ret = iio_read_channel_raw(info->iio_channel[PMIC_TEMP], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-pmictemp : %d\n", raw_val); + ret = iio_read_channel_raw(info->iio_channel[SYSTEM_TEMP], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-systtemp : %d\n", raw_val); + ret = iio_read_channel_raw(info->iio_channel[BAT_CHRG_CURR], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-chrgcurr : %d\n", raw_val); + ret = iio_read_channel_raw(info->iio_channel[BAT_D_CURR], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-dchrgcur : %d\n", raw_val); + ret = iio_read_channel_raw(info->iio_channel[BAT_VOLT], &raw_val); + if (ret >= 0) + seq_printf(s, "axp288-battvolt : %d\n", raw_val); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(fuel_gauge_debug); + +static void fuel_gauge_create_debugfs(struct axp288_fg_info *info) +{ + info->debug_file = debugfs_create_file("fuelgauge", 0666, NULL, + info, &fuel_gauge_debug_fops); +} + +static void fuel_gauge_remove_debugfs(struct axp288_fg_info *info) +{ + debugfs_remove(info->debug_file); +} +#else +static inline void fuel_gauge_create_debugfs(struct axp288_fg_info *info) +{ +} +static inline void fuel_gauge_remove_debugfs(struct axp288_fg_info *info) +{ +} +#endif + +static void fuel_gauge_get_status(struct axp288_fg_info *info) +{ + int pwr_stat, fg_res, curr, ret; + + pwr_stat = fuel_gauge_reg_readb(info, AXP20X_PWR_INPUT_STATUS); + if (pwr_stat < 0) { + dev_err(&info->pdev->dev, + "PWR STAT read failed:%d\n", pwr_stat); + return; + } + + /* Report full if Vbus is valid and the reported capacity is 100% */ + if (!(pwr_stat & PS_STAT_VBUS_VALID)) + goto not_full; + + fg_res = fuel_gauge_reg_readb(info, AXP20X_FG_RES); + if (fg_res < 0) { + dev_err(&info->pdev->dev, "FG RES read failed: %d\n", fg_res); + return; + } + if (!(fg_res & FG_REP_CAP_VALID)) + goto not_full; + + fg_res &= ~FG_REP_CAP_VALID; + if (fg_res == 100) { + info->status = POWER_SUPPLY_STATUS_FULL; + return; + } + + /* + * Sometimes the charger turns itself off before fg-res reaches 100%. + * When this happens the AXP288 reports a not-charging status and + * 0 mA discharge current. + */ + if (fg_res < 90 || (pwr_stat & PS_STAT_BAT_CHRG_DIR)) + goto not_full; + + ret = iio_read_channel_raw(info->iio_channel[BAT_D_CURR], &curr); + if (ret < 0) { + dev_err(&info->pdev->dev, "FG get current failed: %d\n", ret); + return; + } + if (curr == 0) { + info->status = POWER_SUPPLY_STATUS_FULL; + return; + } + +not_full: + if (pwr_stat & PS_STAT_BAT_CHRG_DIR) + info->status = POWER_SUPPLY_STATUS_CHARGING; + else + info->status = POWER_SUPPLY_STATUS_DISCHARGING; +} + +static int fuel_gauge_get_vbatt(struct axp288_fg_info *info, int *vbatt) +{ + int ret = 0, raw_val; + + ret = iio_read_channel_raw(info->iio_channel[BAT_VOLT], &raw_val); + if (ret < 0) + goto vbatt_read_fail; + + *vbatt = VOLTAGE_FROM_ADC(raw_val); +vbatt_read_fail: + return ret; +} + +static int fuel_gauge_get_current(struct axp288_fg_info *info, int *cur) +{ + int ret, discharge; + + /* First check discharge current, so that we do only 1 read on bat. */ + ret = iio_read_channel_raw(info->iio_channel[BAT_D_CURR], &discharge); + if (ret < 0) + return ret; + + if (discharge > 0) { + *cur = -1 * discharge; + return 0; + } + + return iio_read_channel_raw(info->iio_channel[BAT_CHRG_CURR], cur); +} + +static int fuel_gauge_get_vocv(struct axp288_fg_info *info, int *vocv) +{ + int ret; + + ret = fuel_gauge_read_12bit_word(info, AXP288_FG_OCVH_REG); + if (ret >= 0) + *vocv = VOLTAGE_FROM_ADC(ret); + + return ret; +} + +static int fuel_gauge_battery_health(struct axp288_fg_info *info) +{ + int ret, vocv, health = POWER_SUPPLY_HEALTH_UNKNOWN; + + ret = fuel_gauge_get_vocv(info, &vocv); + if (ret < 0) + goto health_read_fail; + + if (vocv > info->max_volt) + health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + health = POWER_SUPPLY_HEALTH_GOOD; + +health_read_fail: + return health; +} + +static int fuel_gauge_get_property(struct power_supply *ps, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct axp288_fg_info *info = power_supply_get_drvdata(ps); + int ret = 0, value; + + mutex_lock(&info->lock); + switch (prop) { + case POWER_SUPPLY_PROP_STATUS: + fuel_gauge_get_status(info); + val->intval = info->status; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = fuel_gauge_battery_health(info); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = fuel_gauge_get_vbatt(info, &value); + if (ret < 0) + goto fuel_gauge_read_err; + val->intval = PROP_VOLT(value); + break; + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + ret = fuel_gauge_get_vocv(info, &value); + if (ret < 0) + goto fuel_gauge_read_err; + val->intval = PROP_VOLT(value); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = fuel_gauge_get_current(info, &value); + if (ret < 0) + goto fuel_gauge_read_err; + val->intval = PROP_CURR(value); + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = fuel_gauge_reg_readb(info, AXP20X_PWR_OP_MODE); + if (ret < 0) + goto fuel_gauge_read_err; + + if (ret & CHRG_STAT_BAT_PRESENT) + val->intval = 1; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = fuel_gauge_reg_readb(info, AXP20X_FG_RES); + if (ret < 0) + goto fuel_gauge_read_err; + + if (!(ret & FG_REP_CAP_VALID)) + dev_err(&info->pdev->dev, + "capacity measurement not valid\n"); + val->intval = (ret & FG_REP_CAP_VAL_MASK); + break; + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + ret = fuel_gauge_reg_readb(info, AXP288_FG_LOW_CAP_REG); + if (ret < 0) + goto fuel_gauge_read_err; + val->intval = (ret & 0x0f); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = fuel_gauge_read_15bit_word(info, AXP288_FG_CC_MTR1_REG); + if (ret < 0) + goto fuel_gauge_read_err; + + val->intval = ret * FG_DES_CAP_RES_LSB; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = fuel_gauge_read_15bit_word(info, AXP288_FG_DES_CAP1_REG); + if (ret < 0) + goto fuel_gauge_read_err; + + val->intval = ret * FG_DES_CAP_RES_LSB; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = PROP_VOLT(info->max_volt); + break; + default: + mutex_unlock(&info->lock); + return -EINVAL; + } + + mutex_unlock(&info->lock); + return 0; + +fuel_gauge_read_err: + mutex_unlock(&info->lock); + return ret; +} + +static int fuel_gauge_set_property(struct power_supply *ps, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct axp288_fg_info *info = power_supply_get_drvdata(ps); + int ret = 0; + + mutex_lock(&info->lock); + switch (prop) { + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + if ((val->intval < 0) || (val->intval > 15)) { + ret = -EINVAL; + break; + } + ret = fuel_gauge_reg_readb(info, AXP288_FG_LOW_CAP_REG); + if (ret < 0) + break; + ret &= 0xf0; + ret |= (val->intval & 0xf); + ret = fuel_gauge_reg_writeb(info, AXP288_FG_LOW_CAP_REG, ret); + break; + default: + ret = -EINVAL; + break; + } + + mutex_unlock(&info->lock); + return ret; +} + +static int fuel_gauge_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} + +static irqreturn_t fuel_gauge_thread_handler(int irq, void *dev) +{ + struct axp288_fg_info *info = dev; + int i; + + for (i = 0; i < AXP288_FG_INTR_NUM; i++) { + if (info->irq[i] == irq) + break; + } + + if (i >= AXP288_FG_INTR_NUM) { + dev_warn(&info->pdev->dev, "spurious interrupt!!\n"); + return IRQ_NONE; + } + + switch (i) { + case QWBTU_IRQ: + dev_info(&info->pdev->dev, + "Quit Battery under temperature in work mode IRQ (QWBTU)\n"); + break; + case WBTU_IRQ: + dev_info(&info->pdev->dev, + "Battery under temperature in work mode IRQ (WBTU)\n"); + break; + case QWBTO_IRQ: + dev_info(&info->pdev->dev, + "Quit Battery over temperature in work mode IRQ (QWBTO)\n"); + break; + case WBTO_IRQ: + dev_info(&info->pdev->dev, + "Battery over temperature in work mode IRQ (WBTO)\n"); + break; + case WL2_IRQ: + dev_info(&info->pdev->dev, "Low Batt Warning(2) INTR\n"); + break; + case WL1_IRQ: + dev_info(&info->pdev->dev, "Low Batt Warning(1) INTR\n"); + break; + default: + dev_warn(&info->pdev->dev, "Spurious Interrupt!!!\n"); + } + + power_supply_changed(info->bat); + return IRQ_HANDLED; +} + +static void fuel_gauge_external_power_changed(struct power_supply *psy) +{ + struct axp288_fg_info *info = power_supply_get_drvdata(psy); + + power_supply_changed(info->bat); +} + +static const struct power_supply_desc fuel_gauge_desc = { + .name = DEV_NAME, + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = fuel_gauge_props, + .num_properties = ARRAY_SIZE(fuel_gauge_props), + .get_property = fuel_gauge_get_property, + .set_property = fuel_gauge_set_property, + .property_is_writeable = fuel_gauge_property_is_writeable, + .external_power_changed = fuel_gauge_external_power_changed, +}; + +static void fuel_gauge_init_irq(struct axp288_fg_info *info) +{ + int ret, i, pirq; + + for (i = 0; i < AXP288_FG_INTR_NUM; i++) { + pirq = platform_get_irq(info->pdev, i); + info->irq[i] = regmap_irq_get_virq(info->regmap_irqc, pirq); + if (info->irq[i] < 0) { + dev_warn(&info->pdev->dev, + "regmap_irq get virq failed for IRQ %d: %d\n", + pirq, info->irq[i]); + info->irq[i] = -1; + goto intr_failed; + } + ret = request_threaded_irq(info->irq[i], + NULL, fuel_gauge_thread_handler, + IRQF_ONESHOT, DEV_NAME, info); + if (ret) { + dev_warn(&info->pdev->dev, + "request irq failed for IRQ %d: %d\n", + pirq, info->irq[i]); + info->irq[i] = -1; + goto intr_failed; + } else { + dev_info(&info->pdev->dev, "HW IRQ %d -> VIRQ %d\n", + pirq, info->irq[i]); + } + } + return; + +intr_failed: + for (; i > 0; i--) { + free_irq(info->irq[i - 1], info); + info->irq[i - 1] = -1; + } +} + +/* + * Some devices have no battery (HDMI sticks) and the axp288 battery's + * detection reports one despite it not being there. + * Please keep this listed sorted alphabetically. + */ +static const struct dmi_system_id axp288_fuel_gauge_blacklist[] = { + { + /* ACEPC T8 Cherry Trail Z8350 mini PC */ + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "To be filled by O.E.M."), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "T8"), + /* also match on somewhat unique bios-version */ + DMI_EXACT_MATCH(DMI_BIOS_VERSION, "1.000"), + }, + }, + { + /* ACEPC T11 Cherry Trail Z8350 mini PC */ + .matches = { + DMI_EXACT_MATCH(DMI_BOARD_VENDOR, "To be filled by O.E.M."), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "Cherry Trail CR"), + DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "T11"), + /* also match on somewhat unique bios-version */ + DMI_EXACT_MATCH(DMI_BIOS_VERSION, "1.000"), + }, + }, + { + /* ECS EF20EA */ + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "EF20EA"), + }, + }, + { + /* Intel Cherry Trail Compute Stick, Windows version */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel"), + DMI_MATCH(DMI_PRODUCT_NAME, "STK1AW32SC"), + }, + }, + { + /* Intel Cherry Trail Compute Stick, version without an OS */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel"), + DMI_MATCH(DMI_PRODUCT_NAME, "STK1A32SC"), + }, + }, + { + /* Meegopad T02 */ + .matches = { + DMI_MATCH(DMI_PRODUCT_NAME, "MEEGOPAD T02"), + }, + }, + { + /* Meegopad T08 */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Default string"), + DMI_MATCH(DMI_BOARD_VENDOR, "To be filled by OEM."), + DMI_MATCH(DMI_BOARD_NAME, "T3 MRD"), + DMI_MATCH(DMI_BOARD_VERSION, "V1.1"), + }, + }, + { + /* Minix Neo Z83-4 mini PC */ + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "MINIX"), + DMI_MATCH(DMI_PRODUCT_NAME, "Z83-4"), + } + }, + {} +}; + +static int axp288_fuel_gauge_probe(struct platform_device *pdev) +{ + int i, ret = 0; + struct axp288_fg_info *info; + struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + static const char * const iio_chan_name[] = { + [BAT_TEMP] = "axp288-batt-temp", + [PMIC_TEMP] = "axp288-pmic-temp", + [SYSTEM_TEMP] = "axp288-system-temp", + [BAT_CHRG_CURR] = "axp288-chrg-curr", + [BAT_D_CURR] = "axp288-chrg-d-curr", + [BAT_VOLT] = "axp288-batt-volt", + }; + unsigned int val; + + if (dmi_check_system(axp288_fuel_gauge_blacklist)) + return -ENODEV; + + /* + * On some devices the fuelgauge and charger parts of the axp288 are + * not used, check that the fuelgauge is enabled (CC_CTRL != 0). + */ + ret = regmap_read(axp20x->regmap, AXP20X_CC_CTRL, &val); + if (ret < 0) + return ret; + if (val == 0) + return -ENODEV; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->pdev = pdev; + info->regmap = axp20x->regmap; + info->regmap_irqc = axp20x->regmap_irqc; + info->status = POWER_SUPPLY_STATUS_UNKNOWN; + + platform_set_drvdata(pdev, info); + + mutex_init(&info->lock); + + for (i = 0; i < IIO_CHANNEL_NUM; i++) { + /* + * Note cannot use devm_iio_channel_get because x86 systems + * lack the device<->channel maps which iio_channel_get will + * try to use when passed a non NULL device pointer. + */ + info->iio_channel[i] = + iio_channel_get(NULL, iio_chan_name[i]); + if (IS_ERR(info->iio_channel[i])) { + ret = PTR_ERR(info->iio_channel[i]); + dev_dbg(&pdev->dev, "error getting iiochan %s: %d\n", + iio_chan_name[i], ret); + /* Wait for axp288_adc to load */ + if (ret == -ENODEV) + ret = -EPROBE_DEFER; + + goto out_free_iio_chan; + } + } + + ret = fuel_gauge_reg_readb(info, AXP288_FG_DES_CAP1_REG); + if (ret < 0) + goto out_free_iio_chan; + + if (!(ret & FG_DES_CAP1_VALID)) { + dev_err(&pdev->dev, "axp288 not configured by firmware\n"); + ret = -ENODEV; + goto out_free_iio_chan; + } + + ret = fuel_gauge_reg_readb(info, AXP20X_CHRG_CTRL1); + if (ret < 0) + goto out_free_iio_chan; + switch ((ret & CHRG_CCCV_CV_MASK) >> CHRG_CCCV_CV_BIT_POS) { + case CHRG_CCCV_CV_4100MV: + info->max_volt = 4100; + break; + case CHRG_CCCV_CV_4150MV: + info->max_volt = 4150; + break; + case CHRG_CCCV_CV_4200MV: + info->max_volt = 4200; + break; + case CHRG_CCCV_CV_4350MV: + info->max_volt = 4350; + break; + } + + psy_cfg.drv_data = info; + info->bat = power_supply_register(&pdev->dev, &fuel_gauge_desc, &psy_cfg); + if (IS_ERR(info->bat)) { + ret = PTR_ERR(info->bat); + dev_err(&pdev->dev, "failed to register battery: %d\n", ret); + goto out_free_iio_chan; + } + + fuel_gauge_create_debugfs(info); + fuel_gauge_init_irq(info); + + return 0; + +out_free_iio_chan: + for (i = 0; i < IIO_CHANNEL_NUM; i++) + if (!IS_ERR_OR_NULL(info->iio_channel[i])) + iio_channel_release(info->iio_channel[i]); + + return ret; +} + +static const struct platform_device_id axp288_fg_id_table[] = { + { .name = DEV_NAME }, + {}, +}; +MODULE_DEVICE_TABLE(platform, axp288_fg_id_table); + +static int axp288_fuel_gauge_remove(struct platform_device *pdev) +{ + struct axp288_fg_info *info = platform_get_drvdata(pdev); + int i; + + power_supply_unregister(info->bat); + fuel_gauge_remove_debugfs(info); + + for (i = 0; i < AXP288_FG_INTR_NUM; i++) + if (info->irq[i] >= 0) + free_irq(info->irq[i], info); + + for (i = 0; i < IIO_CHANNEL_NUM; i++) + iio_channel_release(info->iio_channel[i]); + + return 0; +} + +static struct platform_driver axp288_fuel_gauge_driver = { + .probe = axp288_fuel_gauge_probe, + .remove = axp288_fuel_gauge_remove, + .id_table = axp288_fg_id_table, + .driver = { + .name = DEV_NAME, + }, +}; + +module_platform_driver(axp288_fuel_gauge_driver); + +MODULE_AUTHOR("Ramakrishna Pallala <ramakrishna.pallala@intel.com>"); +MODULE_AUTHOR("Todd Brandt <todd.e.brandt@linux.intel.com>"); +MODULE_DESCRIPTION("Xpower AXP288 Fuel Gauge Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bd70528-charger.c b/drivers/power/supply/bd70528-charger.c new file mode 100644 index 000000000..7c1f0b99c --- /dev/null +++ b/drivers/power/supply/bd70528-charger.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Copyright (C) 2018 ROHM Semiconductors +// +// power-supply driver for ROHM BD70528 PMIC + +/* + * BD70528 charger HW state machine. + * + * The thermal shutdown state is not drawn. From any other state but + * battery error and suspend it is possible to go to TSD/TMP states + * if temperature is out of bounds. + * + * CHG_RST = H + * or CHG_EN=L + * or (DCIN2_UVLO=L && DCIN1_UVLO=L) + * or (DCIN2_OVLO=H & DCIN1_UVKLO=L) + * + * +--------------+ +--------------+ + * | | | | + * | Any state +-------> | Suspend | + * | | | | + * +--------------+ +------+-------+ + * | + * CHG_EN = H && BAT_DET = H && | + * No errors (temp, bat_ov, UVLO, | + * OVLO...) | + * | + * BAT_OV or +---------v----------+ + * (DBAT && TTRI) | | + * +-----------------+ Trickle Charge | <---------------+ + * | | | | + * | +-------+------------+ | + * | | | + * | | ^ | + * | V_BAT > VTRI_TH | | VBAT < VTRI_TH - 50mV | + * | | | | + * | v | | + * | | | + * | BAT_OV or +----------+----+ | + * | (DBAT && TFST) | | | + * | +----------------+ Fast Charge | | + * | | | | | + * v v +----+----------+ | + * | | + *+----------------+ ILIM_DET=L | ^ ILIM_DET | + *| | & CV_DET=H | | or CV_DET=L | + *| Battery Error | & VBAT > | | or VBAT < VRECHG_TH | + *| | VRECHG_TH | | or IBAT > IFST/x | + *+----------------+ & IBAT < | | | + * IFST/x v | | + * ^ | | + * | +---------+-+ | + * | | | | + * +-------------------+ Top OFF | | + * BAT_OV = H or | | | + * (DBAT && TFST) +-----+-----+ | + * | | + * Stay top-off for 15s | | + * v | + * | + * +--------+ | + * | | | + * | Done +-------------------------+ + * | | + * +--------+ VBAT < VRECHG_TH + */ + +#include <linux/kernel.h> +#include <linux/interrupt.h> +#include <linux/mfd/rohm-bd70528.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/linear_range.h> + +#define CHG_STAT_SUSPEND 0x0 +#define CHG_STAT_TRICKLE 0x1 +#define CHG_STAT_FAST 0x3 +#define CHG_STAT_TOPOFF 0xe +#define CHG_STAT_DONE 0xf +#define CHG_STAT_OTP_TRICKLE 0x10 +#define CHG_STAT_OTP_FAST 0x11 +#define CHG_STAT_OTP_DONE 0x12 +#define CHG_STAT_TSD_TRICKLE 0x20 +#define CHG_STAT_TSD_FAST 0x21 +#define CHG_STAT_TSD_TOPOFF 0x22 +#define CHG_STAT_BAT_ERR 0x7f + +static const char *bd70528_charger_model = "BD70528"; +static const char *bd70528_charger_manufacturer = "ROHM Semiconductors"; + +#define BD_ERR_IRQ_HND(_name_, _wrn_) \ +static irqreturn_t bd0528_##_name_##_interrupt(int irq, void *arg) \ +{ \ + struct power_supply *psy = (struct power_supply *)arg; \ + \ + power_supply_changed(psy); \ + dev_err(&psy->dev, (_wrn_)); \ + \ + return IRQ_HANDLED; \ +} + +#define BD_INFO_IRQ_HND(_name_, _wrn_) \ +static irqreturn_t bd0528_##_name_##_interrupt(int irq, void *arg) \ +{ \ + struct power_supply *psy = (struct power_supply *)arg; \ + \ + power_supply_changed(psy); \ + dev_dbg(&psy->dev, (_wrn_)); \ + \ + return IRQ_HANDLED; \ +} + +#define BD_IRQ_HND(_name_) bd0528_##_name_##_interrupt + +struct bd70528_psy { + struct regmap *regmap; + struct device *dev; + struct power_supply *psy; +}; + +BD_ERR_IRQ_HND(BAT_OV_DET, "Battery overvoltage detected\n"); +BD_ERR_IRQ_HND(DBAT_DET, "Dead battery detected\n"); +BD_ERR_IRQ_HND(COLD_DET, "Battery cold\n"); +BD_ERR_IRQ_HND(HOT_DET, "Battery hot\n"); +BD_ERR_IRQ_HND(CHG_TSD, "Charger thermal shutdown\n"); +BD_ERR_IRQ_HND(DCIN2_OV_DET, "DCIN2 overvoltage detected\n"); + +BD_INFO_IRQ_HND(BAT_OV_RES, "Battery voltage back to normal\n"); +BD_INFO_IRQ_HND(COLD_RES, "Battery temperature back to normal\n"); +BD_INFO_IRQ_HND(HOT_RES, "Battery temperature back to normal\n"); +BD_INFO_IRQ_HND(BAT_RMV, "Battery removed\n"); +BD_INFO_IRQ_HND(BAT_DET, "Battery detected\n"); +BD_INFO_IRQ_HND(DCIN2_OV_RES, "DCIN2 voltage back to normal\n"); +BD_INFO_IRQ_HND(DCIN2_RMV, "DCIN2 removed\n"); +BD_INFO_IRQ_HND(DCIN2_DET, "DCIN2 detected\n"); +BD_INFO_IRQ_HND(DCIN1_RMV, "DCIN1 removed\n"); +BD_INFO_IRQ_HND(DCIN1_DET, "DCIN1 detected\n"); + +struct irq_name_pair { + const char *n; + irqreturn_t (*h)(int irq, void *arg); +}; + +static int bd70528_get_irqs(struct platform_device *pdev, + struct bd70528_psy *bdpsy) +{ + int irq, i, ret; + unsigned int mask; + static const struct irq_name_pair bd70528_chg_irqs[] = { + { .n = "bd70528-bat-ov-res", .h = BD_IRQ_HND(BAT_OV_RES) }, + { .n = "bd70528-bat-ov-det", .h = BD_IRQ_HND(BAT_OV_DET) }, + { .n = "bd70528-bat-dead", .h = BD_IRQ_HND(DBAT_DET) }, + { .n = "bd70528-bat-warmed", .h = BD_IRQ_HND(COLD_RES) }, + { .n = "bd70528-bat-cold", .h = BD_IRQ_HND(COLD_DET) }, + { .n = "bd70528-bat-cooled", .h = BD_IRQ_HND(HOT_RES) }, + { .n = "bd70528-bat-hot", .h = BD_IRQ_HND(HOT_DET) }, + { .n = "bd70528-chg-tshd", .h = BD_IRQ_HND(CHG_TSD) }, + { .n = "bd70528-bat-removed", .h = BD_IRQ_HND(BAT_RMV) }, + { .n = "bd70528-bat-detected", .h = BD_IRQ_HND(BAT_DET) }, + { .n = "bd70528-dcin2-ov-res", .h = BD_IRQ_HND(DCIN2_OV_RES) }, + { .n = "bd70528-dcin2-ov-det", .h = BD_IRQ_HND(DCIN2_OV_DET) }, + { .n = "bd70528-dcin2-removed", .h = BD_IRQ_HND(DCIN2_RMV) }, + { .n = "bd70528-dcin2-detected", .h = BD_IRQ_HND(DCIN2_DET) }, + { .n = "bd70528-dcin1-removed", .h = BD_IRQ_HND(DCIN1_RMV) }, + { .n = "bd70528-dcin1-detected", .h = BD_IRQ_HND(DCIN1_DET) }, + }; + + for (i = 0; i < ARRAY_SIZE(bd70528_chg_irqs); i++) { + irq = platform_get_irq_byname(pdev, bd70528_chg_irqs[i].n); + if (irq < 0) { + dev_err(&pdev->dev, "Bad IRQ information for %s (%d)\n", + bd70528_chg_irqs[i].n, irq); + return irq; + } + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + bd70528_chg_irqs[i].h, + IRQF_ONESHOT, + bd70528_chg_irqs[i].n, + bdpsy->psy); + + if (ret) + return ret; + } + /* + * BD70528 irq controller is not touching the main mask register. + * So enable the charger block interrupts at main level. We can just + * leave them enabled as irq-controller should disable irqs + * from sub-registers when IRQ is disabled or freed. + */ + mask = BD70528_REG_INT_BAT1_MASK | BD70528_REG_INT_BAT2_MASK; + ret = regmap_update_bits(bdpsy->regmap, + BD70528_REG_INT_MAIN_MASK, mask, 0); + if (ret) + dev_err(&pdev->dev, "Failed to enable charger IRQs\n"); + + return ret; +} + +static int bd70528_get_charger_status(struct bd70528_psy *bdpsy, int *val) +{ + int ret; + unsigned int v; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_CURR_STAT, &v); + if (ret) { + dev_err(bdpsy->dev, "Charger state read failure %d\n", + ret); + return ret; + } + + switch (v & BD70528_MASK_CHG_STAT) { + case CHG_STAT_SUSPEND: + /* Maybe we should check the CHG_TTRI_EN? */ + case CHG_STAT_OTP_TRICKLE: + case CHG_STAT_OTP_FAST: + case CHG_STAT_OTP_DONE: + case CHG_STAT_TSD_TRICKLE: + case CHG_STAT_TSD_FAST: + case CHG_STAT_TSD_TOPOFF: + case CHG_STAT_BAT_ERR: + *val = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case CHG_STAT_DONE: + *val = POWER_SUPPLY_STATUS_FULL; + break; + case CHG_STAT_TRICKLE: + case CHG_STAT_FAST: + case CHG_STAT_TOPOFF: + *val = POWER_SUPPLY_STATUS_CHARGING; + break; + default: + *val = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + + return 0; +} + +static int bd70528_get_charge_type(struct bd70528_psy *bdpsy, int *val) +{ + int ret; + unsigned int v; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_CURR_STAT, &v); + if (ret) { + dev_err(bdpsy->dev, "Charger state read failure %d\n", + ret); + return ret; + } + + switch (v & BD70528_MASK_CHG_STAT) { + case CHG_STAT_TRICKLE: + *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case CHG_STAT_FAST: + case CHG_STAT_TOPOFF: + *val = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case CHG_STAT_DONE: + case CHG_STAT_SUSPEND: + /* Maybe we should check the CHG_TTRI_EN? */ + case CHG_STAT_OTP_TRICKLE: + case CHG_STAT_OTP_FAST: + case CHG_STAT_OTP_DONE: + case CHG_STAT_TSD_TRICKLE: + case CHG_STAT_TSD_FAST: + case CHG_STAT_TSD_TOPOFF: + case CHG_STAT_BAT_ERR: + *val = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + default: + *val = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + break; + } + + return 0; +} + +static int bd70528_get_battery_health(struct bd70528_psy *bdpsy, int *val) +{ + int ret; + unsigned int v; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_BAT_STAT, &v); + if (ret) { + dev_err(bdpsy->dev, "Battery state read failure %d\n", + ret); + return ret; + } + /* No battery? */ + if (!(v & BD70528_MASK_CHG_BAT_DETECT)) + *val = POWER_SUPPLY_HEALTH_DEAD; + else if (v & BD70528_MASK_CHG_BAT_OVERVOLT) + *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (v & BD70528_MASK_CHG_BAT_TIMER) + *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + else + *val = POWER_SUPPLY_HEALTH_GOOD; + + return 0; +} + +static int bd70528_get_online(struct bd70528_psy *bdpsy, int *val) +{ + int ret; + unsigned int v; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_IN_STAT, &v); + if (ret) { + dev_err(bdpsy->dev, "DC1 IN state read failure %d\n", + ret); + return ret; + } + + *val = (v & BD70528_MASK_CHG_DCIN1_UVLO) ? 1 : 0; + + return 0; +} + +static int bd70528_get_present(struct bd70528_psy *bdpsy, int *val) +{ + int ret; + unsigned int v; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_BAT_STAT, &v); + if (ret) { + dev_err(bdpsy->dev, "Battery state read failure %d\n", + ret); + return ret; + } + + *val = (v & BD70528_MASK_CHG_BAT_DETECT) ? 1 : 0; + + return 0; +} + +static const struct linear_range current_limit_ranges[] = { + { + .min = 5, + .step = 1, + .min_sel = 0, + .max_sel = 0x22, + }, + { + .min = 40, + .step = 5, + .min_sel = 0x23, + .max_sel = 0x26, + }, + { + .min = 60, + .step = 20, + .min_sel = 0x27, + .max_sel = 0x2d, + }, + { + .min = 200, + .step = 50, + .min_sel = 0x2e, + .max_sel = 0x34, + }, + { + .min = 500, + .step = 0, + .min_sel = 0x35, + .max_sel = 0x3f, + }, +}; + +/* + * BD70528 would support setting and getting own charge current/ + * voltage for low temperatures. The driver currently only reads + * the charge current at room temperature. We do set both though. + */ +static const struct linear_range warm_charge_curr[] = { + { + .min = 10, + .step = 10, + .min_sel = 0, + .max_sel = 0x12, + }, + { + .min = 200, + .step = 25, + .min_sel = 0x13, + .max_sel = 0x1f, + }, +}; + +/* + * Cold charge current selectors are identical to warm charge current + * selectors. The difference is that only smaller currents are available + * at cold charge range. + */ +#define MAX_COLD_CHG_CURR_SEL 0x15 +#define MAX_WARM_CHG_CURR_SEL 0x1f +#define MIN_CHG_CURR_SEL 0x0 + +static int get_charge_current(struct bd70528_psy *bdpsy, int *ma) +{ + unsigned int sel; + int ret; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_CHG_CURR_WARM, + &sel); + if (ret) { + dev_err(bdpsy->dev, + "Charge current reading failed (%d)\n", ret); + return ret; + } + + sel &= BD70528_MASK_CHG_CHG_CURR; + + ret = linear_range_get_value_array(&warm_charge_curr[0], + ARRAY_SIZE(warm_charge_curr), + sel, ma); + if (ret) { + dev_err(bdpsy->dev, + "Unknown charge current value 0x%x\n", + sel); + } + + return ret; +} + +static int get_current_limit(struct bd70528_psy *bdpsy, int *ma) +{ + unsigned int sel; + int ret; + + ret = regmap_read(bdpsy->regmap, BD70528_REG_CHG_DCIN_ILIM, + &sel); + + if (ret) { + dev_err(bdpsy->dev, + "Input current limit reading failed (%d)\n", ret); + return ret; + } + + sel &= BD70528_MASK_CHG_DCIN_ILIM; + + ret = linear_range_get_value_array(¤t_limit_ranges[0], + ARRAY_SIZE(current_limit_ranges), + sel, ma); + if (ret) { + /* Unspecified values mean 500 mA */ + *ma = 500; + } + return 0; +} + +static enum power_supply_property bd70528_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static int bd70528_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bd70528_psy *bdpsy = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + return bd70528_get_charger_status(bdpsy, &val->intval); + case POWER_SUPPLY_PROP_CHARGE_TYPE: + return bd70528_get_charge_type(bdpsy, &val->intval); + case POWER_SUPPLY_PROP_HEALTH: + return bd70528_get_battery_health(bdpsy, &val->intval); + case POWER_SUPPLY_PROP_PRESENT: + return bd70528_get_present(bdpsy, &val->intval); + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = get_current_limit(bdpsy, &val->intval); + val->intval *= 1000; + return ret; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = get_charge_current(bdpsy, &val->intval); + val->intval *= 1000; + return ret; + case POWER_SUPPLY_PROP_ONLINE: + return bd70528_get_online(bdpsy, &val->intval); + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bd70528_charger_model; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = bd70528_charger_manufacturer; + return 0; + default: + break; + } + + return -EINVAL; +} + +static int bd70528_prop_is_writable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return 1; + default: + break; + } + return 0; +} + +static int set_charge_current(struct bd70528_psy *bdpsy, int ma) +{ + unsigned int reg; + int ret = 0, tmpret; + bool found; + + if (ma > 500) { + dev_warn(bdpsy->dev, + "Requested charge current %u exceed maximum (500mA)\n", + ma); + reg = MAX_WARM_CHG_CURR_SEL; + goto set; + } + if (ma < 10) { + dev_err(bdpsy->dev, + "Requested charge current %u smaller than min (10mA)\n", + ma); + reg = MIN_CHG_CURR_SEL; + ret = -EINVAL; + goto set; + } + +/* + * For BD70528 voltage/current limits we happily accept any value which + * belongs the range. We could check if value matching the selector is + * desired by computing the range min + (sel - sel_low) * range step - but + * I guess it is enough if we use voltage/current which is closest (below) + * the requested? + */ + + ret = linear_range_get_selector_low_array(warm_charge_curr, + ARRAY_SIZE(warm_charge_curr), + ma, ®, &found); + if (ret) { + dev_err(bdpsy->dev, + "Unsupported charge current %u mA\n", ma); + reg = MIN_CHG_CURR_SEL; + goto set; + } + if (!found) { + /* + * There was a gap in supported values and we hit it. + * Yet a smaller value was found so we use it. + */ + dev_warn(bdpsy->dev, + "Unsupported charge current %u mA\n", ma); + } +set: + + tmpret = regmap_update_bits(bdpsy->regmap, + BD70528_REG_CHG_CHG_CURR_WARM, + BD70528_MASK_CHG_CHG_CURR, reg); + if (tmpret) + dev_err(bdpsy->dev, + "Charge current write failure (%d)\n", tmpret); + + if (reg > MAX_COLD_CHG_CURR_SEL) + reg = MAX_COLD_CHG_CURR_SEL; + + if (!tmpret) + tmpret = regmap_update_bits(bdpsy->regmap, + BD70528_REG_CHG_CHG_CURR_COLD, + BD70528_MASK_CHG_CHG_CURR, reg); + + if (!ret) + ret = tmpret; + + return ret; +} + +#define MAX_CURR_LIMIT_SEL 0x34 +#define MIN_CURR_LIMIT_SEL 0x0 + +static int set_current_limit(struct bd70528_psy *bdpsy, int ma) +{ + unsigned int reg; + int ret = 0, tmpret; + bool found; + + if (ma > 500) { + dev_warn(bdpsy->dev, + "Requested current limit %u exceed maximum (500mA)\n", + ma); + reg = MAX_CURR_LIMIT_SEL; + goto set; + } + if (ma < 5) { + dev_err(bdpsy->dev, + "Requested current limit %u smaller than min (5mA)\n", + ma); + reg = MIN_CURR_LIMIT_SEL; + ret = -EINVAL; + goto set; + } + + ret = linear_range_get_selector_low_array(current_limit_ranges, + ARRAY_SIZE(current_limit_ranges), + ma, ®, &found); + if (ret) { + dev_err(bdpsy->dev, "Unsupported current limit %umA\n", ma); + reg = MIN_CURR_LIMIT_SEL; + goto set; + } + if (!found) { + /* + * There was a gap in supported values and we hit it. + * We found a smaller value from ranges and use it. + * Warn user though. + */ + dev_warn(bdpsy->dev, "Unsupported current limit %umA\n", ma); + } + +set: + tmpret = regmap_update_bits(bdpsy->regmap, + BD70528_REG_CHG_DCIN_ILIM, + BD70528_MASK_CHG_DCIN_ILIM, reg); + + if (!ret) + ret = tmpret; + + return ret; +} + +static int bd70528_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct bd70528_psy *bdpsy = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return set_current_limit(bdpsy, val->intval / 1000); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return set_charge_current(bdpsy, val->intval / 1000); + default: + break; + } + return -EINVAL; +} + +static const struct power_supply_desc bd70528_charger_desc = { + .name = "bd70528-charger", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = bd70528_charger_props, + .num_properties = ARRAY_SIZE(bd70528_charger_props), + .get_property = bd70528_charger_get_property, + .set_property = bd70528_charger_set_property, + .property_is_writeable = bd70528_prop_is_writable, +}; + +static int bd70528_power_probe(struct platform_device *pdev) +{ + struct bd70528_psy *bdpsy; + struct power_supply_config cfg = {}; + + bdpsy = devm_kzalloc(&pdev->dev, sizeof(*bdpsy), GFP_KERNEL); + if (!bdpsy) + return -ENOMEM; + + bdpsy->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!bdpsy->regmap) { + dev_err(&pdev->dev, "No regmap found for chip\n"); + return -EINVAL; + } + bdpsy->dev = &pdev->dev; + + platform_set_drvdata(pdev, bdpsy); + cfg.drv_data = bdpsy; + cfg.of_node = pdev->dev.parent->of_node; + + bdpsy->psy = devm_power_supply_register(&pdev->dev, + &bd70528_charger_desc, &cfg); + if (IS_ERR(bdpsy->psy)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + return PTR_ERR(bdpsy->psy); + } + + return bd70528_get_irqs(pdev, bdpsy); +} + +static struct platform_driver bd70528_power = { + .driver = { + .name = "bd70528-power" + }, + .probe = bd70528_power_probe, +}; + +module_platform_driver(bd70528_power); + +MODULE_AUTHOR("Matti Vaittinen <matti.vaittinen@fi.rohmeurope.com>"); +MODULE_DESCRIPTION("BD70528 power-supply driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:bd70528-power"); diff --git a/drivers/power/supply/bd99954-charger.c b/drivers/power/supply/bd99954-charger.c new file mode 100644 index 000000000..ffd8bfa08 --- /dev/null +++ b/drivers/power/supply/bd99954-charger.c @@ -0,0 +1,1142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ROHM BD99954 charger driver + * + * Copyright (C) 2020 Rohm Semiconductors + * Originally written by: + * Mikko Mutanen <mikko.mutanen@fi.rohmeurope.com> + * Markus Laine <markus.laine@fi.rohmeurope.com> + * Bugs added by: + * Matti Vaittinen <matti.vaittinen@fi.rohmeurope.com> + */ + +/* + * The battery charging profile of BD99954. + * + * Curve (1) represents charging current. + * Curve (2) represents battery voltage. + * + * The BD99954 data sheet divides charging to three phases. + * a) Trickle-charge with constant current (8). + * b) pre-charge with constant current (6) + * c) fast-charge, first with constant current (5) phase. After + * the battery voltage has reached target level (4) we have constant + * voltage phase until charging current has dropped to termination + * level (7) + * + * V ^ ^ I + * . . + * . . + *(4)` `.` ` ` ` ` ` ` ` ` ` ` ` ` ` ----------------------------. + * . :/ . + * . o----+/:/ ` ` ` ` ` ` ` ` ` ` ` ` `.` ` (5) + * . + :: + . + * . + /- -- . + * . +`/- + . + * . o/- -: . + * . .s. +` . + * . .--+ `/ . + * . ..`` + .: . + * . -` + -- . + * . (2) ...`` + :- . + * . ...`` + -: . + *(3)` `.`."" ` ` ` `+-------- ` ` ` ` ` ` `.:` ` ` ` ` ` ` ` ` .` ` (6) + * . + `:. . + * . + -: . + * . + -:. . + * . + .--. . + * . (1) + `.+` ` ` `.` ` (7) + * -..............` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` + ` ` ` .` ` (8) + * . + - + * -------------------------------------------------+++++++++--> + * | trickle | pre | fast | + * + * Details of DT properties for different limits can be found from BD99954 + * device tree binding documentation. + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/linear_range.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/regmap.h> +#include <linux/types.h> + +#include "bd99954-charger.h" + +struct battery_data { + u16 precharge_current; /* Trickle-charge Current */ + u16 fc_reg_voltage; /* Fast Charging Regulation Voltage */ + u16 voltage_min; + u16 voltage_max; +}; + +/* Initial field values, converted to initial register values */ +struct bd9995x_init_data { + u16 vsysreg_set; /* VSYS Regulation Setting */ + u16 ibus_lim_set; /* VBUS input current limitation */ + u16 icc_lim_set; /* VCC/VACP Input Current Limit Setting */ + u16 itrich_set; /* Trickle-charge Current Setting */ + u16 iprech_set; /* Pre-Charge Current Setting */ + u16 ichg_set; /* Fast-Charge constant current */ + u16 vfastchg_reg_set1; /* Fast Charging Regulation Voltage */ + u16 vprechg_th_set; /* Pre-charge Voltage Threshold Setting */ + u16 vrechg_set; /* Re-charge Battery Voltage Setting */ + u16 vbatovp_set; /* Battery Over Voltage Threshold Setting */ + u16 iterm_set; /* Charging termination current */ +}; + +struct bd9995x_state { + u8 online; + u16 chgstm_status; + u16 vbat_vsys_status; + u16 vbus_vcc_status; +}; + +struct bd9995x_device { + struct i2c_client *client; + struct device *dev; + struct power_supply *charger; + + struct regmap *rmap; + struct regmap_field *rmap_fields[F_MAX_FIELDS]; + + int chip_id; + int chip_rev; + struct bd9995x_init_data init_data; + struct bd9995x_state state; + + struct mutex lock; /* Protect state data */ +}; + +static const struct regmap_range bd9995x_readonly_reg_ranges[] = { + regmap_reg_range(CHGSTM_STATUS, SEL_ILIM_VAL), + regmap_reg_range(IOUT_DACIN_VAL, IOUT_DACIN_VAL), + regmap_reg_range(VCC_UCD_STATUS, VCC_IDD_STATUS), + regmap_reg_range(VBUS_UCD_STATUS, VBUS_IDD_STATUS), + regmap_reg_range(CHIP_ID, CHIP_REV), + regmap_reg_range(SYSTEM_STATUS, SYSTEM_STATUS), + regmap_reg_range(IBATP_VAL, VBAT_AVE_VAL), + regmap_reg_range(VTH_VAL, EXTIADP_AVE_VAL), +}; + +static const struct regmap_access_table bd9995x_writeable_regs = { + .no_ranges = bd9995x_readonly_reg_ranges, + .n_no_ranges = ARRAY_SIZE(bd9995x_readonly_reg_ranges), +}; + +static const struct regmap_range bd9995x_volatile_reg_ranges[] = { + regmap_reg_range(CHGSTM_STATUS, WDT_STATUS), + regmap_reg_range(VCC_UCD_STATUS, VCC_IDD_STATUS), + regmap_reg_range(VBUS_UCD_STATUS, VBUS_IDD_STATUS), + regmap_reg_range(INT0_STATUS, INT7_STATUS), + regmap_reg_range(SYSTEM_STATUS, SYSTEM_CTRL_SET), + regmap_reg_range(IBATP_VAL, EXTIADP_AVE_VAL), /* Measurement regs */ +}; + +static const struct regmap_access_table bd9995x_volatile_regs = { + .yes_ranges = bd9995x_volatile_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(bd9995x_volatile_reg_ranges), +}; + +static const struct regmap_range_cfg regmap_range_cfg[] = { + { + .selector_reg = MAP_SET, + .selector_mask = 0xFFFF, + .selector_shift = 0, + .window_start = 0, + .window_len = 0x100, + .range_min = 0 * 0x100, + .range_max = 3 * 0x100, + }, +}; + +static const struct regmap_config bd9995x_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + .reg_stride = 1, + + .max_register = 3 * 0x100, + .cache_type = REGCACHE_RBTREE, + + .ranges = regmap_range_cfg, + .num_ranges = ARRAY_SIZE(regmap_range_cfg), + .val_format_endian = REGMAP_ENDIAN_LITTLE, + .wr_table = &bd9995x_writeable_regs, + .volatile_table = &bd9995x_volatile_regs, +}; + +enum bd9995x_chrg_fault { + CHRG_FAULT_NORMAL, + CHRG_FAULT_INPUT, + CHRG_FAULT_THERMAL_SHUTDOWN, + CHRG_FAULT_TIMER_EXPIRED, +}; + +static int bd9995x_get_prop_batt_health(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_BATTEMP], &tmp); + if (ret) + return POWER_SUPPLY_HEALTH_UNKNOWN; + + /* TODO: Check these against datasheet page 34 */ + + switch (tmp) { + case ROOM: + return POWER_SUPPLY_HEALTH_GOOD; + case HOT1: + case HOT2: + case HOT3: + return POWER_SUPPLY_HEALTH_OVERHEAT; + case COLD1: + case COLD2: + return POWER_SUPPLY_HEALTH_COLD; + case TEMP_DIS: + case BATT_OPEN: + default: + return POWER_SUPPLY_HEALTH_UNKNOWN; + } +} + +static int bd9995x_get_prop_charge_type(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_CHGSTM_STATE], &tmp); + if (ret) + return POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + + switch (tmp) { + case CHGSTM_TRICKLE_CHARGE: + case CHGSTM_PRE_CHARGE: + return POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + case CHGSTM_FAST_CHARGE: + return POWER_SUPPLY_CHARGE_TYPE_FAST; + case CHGSTM_TOP_OFF: + case CHGSTM_DONE: + case CHGSTM_SUSPEND: + return POWER_SUPPLY_CHARGE_TYPE_NONE; + default: /* Rest of the states are error related, no charging */ + return POWER_SUPPLY_CHARGE_TYPE_NONE; + } +} + +static bool bd9995x_get_prop_batt_present(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_BATTEMP], &tmp); + if (ret) + return false; + + return tmp != BATT_OPEN; +} + +static int bd9995x_get_prop_batt_voltage(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_VBAT_VAL], &tmp); + if (ret) + return 0; + + tmp = min(tmp, 19200); + + return tmp * 1000; +} + +static int bd9995x_get_prop_batt_current(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_IBATP_VAL], &tmp); + if (ret) + return 0; + + return tmp * 1000; +} + +#define DEFAULT_BATTERY_TEMPERATURE 250 + +static int bd9995x_get_prop_batt_temp(struct bd9995x_device *bd) +{ + int ret, tmp; + + ret = regmap_field_read(bd->rmap_fields[F_THERM_VAL], &tmp); + if (ret) + return DEFAULT_BATTERY_TEMPERATURE; + + return (200 - tmp) * 10; +} + +static int bd9995x_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret, tmp; + struct bd9995x_device *bd = power_supply_get_drvdata(psy); + struct bd9995x_state state; + + mutex_lock(&bd->lock); + state = bd->state; + mutex_unlock(&bd->lock); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + switch (state.chgstm_status) { + case CHGSTM_TRICKLE_CHARGE: + case CHGSTM_PRE_CHARGE: + case CHGSTM_FAST_CHARGE: + case CHGSTM_TOP_OFF: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + + case CHGSTM_DONE: + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + + case CHGSTM_SUSPEND: + case CHGSTM_TEMPERATURE_ERROR_1: + case CHGSTM_TEMPERATURE_ERROR_2: + case CHGSTM_TEMPERATURE_ERROR_3: + case CHGSTM_TEMPERATURE_ERROR_4: + case CHGSTM_TEMPERATURE_ERROR_5: + case CHGSTM_TEMPERATURE_ERROR_6: + case CHGSTM_TEMPERATURE_ERROR_7: + case CHGSTM_THERMAL_SHUT_DOWN_1: + case CHGSTM_THERMAL_SHUT_DOWN_2: + case CHGSTM_THERMAL_SHUT_DOWN_3: + case CHGSTM_THERMAL_SHUT_DOWN_4: + case CHGSTM_THERMAL_SHUT_DOWN_5: + case CHGSTM_THERMAL_SHUT_DOWN_6: + case CHGSTM_THERMAL_SHUT_DOWN_7: + case CHGSTM_BATTERY_ERROR: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BD9995X_MANUFACTURER; + break; + + case POWER_SUPPLY_PROP_ONLINE: + val->intval = state.online; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = regmap_field_read(bd->rmap_fields[F_IBATP_VAL], &tmp); + if (ret) + return ret; + val->intval = tmp * 1000; + break; + + case POWER_SUPPLY_PROP_CHARGE_AVG: + ret = regmap_field_read(bd->rmap_fields[F_IBATP_AVE_VAL], &tmp); + if (ret) + return ret; + val->intval = tmp * 1000; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + /* + * Currently the DT uses this property to give the + * target current for fast-charging constant current phase. + * I think it is correct in a sense. + * + * Yet, this prop we read and return here is the programmed + * safety limit for combined input currents. This feels + * also correct in a sense. + * + * However, this results a mismatch to DT value and value + * read from sysfs. + */ + ret = regmap_field_read(bd->rmap_fields[F_SEL_ILIM_VAL], &tmp); + if (ret) + return ret; + val->intval = tmp * 1000; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + if (!state.online) { + val->intval = 0; + break; + } + + ret = regmap_field_read(bd->rmap_fields[F_VFASTCHG_REG_SET1], + &tmp); + if (ret) + return ret; + + /* + * The actual range : 2560 to 19200 mV. No matter what the + * register says + */ + val->intval = clamp_val(tmp << 4, 2560, 19200); + val->intval *= 1000; + break; + + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + ret = regmap_field_read(bd->rmap_fields[F_ITERM_SET], &tmp); + if (ret) + return ret; + /* Start step is 64 mA */ + val->intval = tmp << 6; + /* Maximum is 1024 mA - no matter what register says */ + val->intval = min(val->intval, 1024); + val->intval *= 1000; + break; + + /* Battery properties which we access through charger */ + case POWER_SUPPLY_PROP_PRESENT: + val->intval = bd9995x_get_prop_batt_present(bd); + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = bd9995x_get_prop_batt_voltage(bd); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = bd9995x_get_prop_batt_current(bd); + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = bd9995x_get_prop_charge_type(bd); + break; + + case POWER_SUPPLY_PROP_HEALTH: + val->intval = bd9995x_get_prop_batt_health(bd); + break; + + case POWER_SUPPLY_PROP_TEMP: + val->intval = bd9995x_get_prop_batt_temp(bd); + break; + + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = "bd99954"; + break; + + default: + return -EINVAL; + + } + + return 0; +} + +static int bd9995x_get_chip_state(struct bd9995x_device *bd, + struct bd9995x_state *state) +{ + int i, ret, tmp; + struct { + struct regmap_field *id; + u16 *data; + } state_fields[] = { + { + bd->rmap_fields[F_CHGSTM_STATE], &state->chgstm_status, + }, { + bd->rmap_fields[F_VBAT_VSYS_STATUS], + &state->vbat_vsys_status, + }, { + bd->rmap_fields[F_VBUS_VCC_STATUS], + &state->vbus_vcc_status, + }, + }; + + + for (i = 0; i < ARRAY_SIZE(state_fields); i++) { + ret = regmap_field_read(state_fields[i].id, &tmp); + if (ret) + return ret; + + *state_fields[i].data = tmp; + } + + if (state->vbus_vcc_status & STATUS_VCC_DET || + state->vbus_vcc_status & STATUS_VBUS_DET) + state->online = 1; + else + state->online = 0; + + return 0; +} + +static irqreturn_t bd9995x_irq_handler_thread(int irq, void *private) +{ + struct bd9995x_device *bd = private; + int ret, status, mask, i; + unsigned long tmp; + struct bd9995x_state state; + + /* + * The bd9995x does not seem to generate big amount of interrupts. + * The logic regarding which interrupts can cause relevant + * status changes seem to be pretty complex. + * + * So lets implement really simple and hopefully bullet-proof handler: + * It does not really matter which IRQ we handle, we just go and + * re-read all interesting statuses + give the framework a nudge. + * + * Other option would be building a _complex_ and error prone logic + * trying to decide what could have been changed (resulting this IRQ + * we are now handling). During the normal operation the BD99954 does + * not seem to be generating much of interrupts so benefit from such + * logic would probably be minimal. + */ + + ret = regmap_read(bd->rmap, INT0_STATUS, &status); + if (ret) { + dev_err(bd->dev, "Failed to read IRQ status\n"); + return IRQ_NONE; + } + + ret = regmap_field_read(bd->rmap_fields[F_INT0_SET], &mask); + if (ret) { + dev_err(bd->dev, "Failed to read IRQ mask\n"); + return IRQ_NONE; + } + + /* Handle only IRQs that are not masked */ + status &= mask; + tmp = status; + + /* Lowest bit does not represent any sub-registers */ + tmp >>= 1; + + /* + * Mask and ack IRQs we will handle (+ the idiot bit) + */ + ret = regmap_field_write(bd->rmap_fields[F_INT0_SET], 0); + if (ret) { + dev_err(bd->dev, "Failed to mask F_INT0\n"); + return IRQ_NONE; + } + + ret = regmap_write(bd->rmap, INT0_STATUS, status); + if (ret) { + dev_err(bd->dev, "Failed to ack F_INT0\n"); + goto err_umask; + } + + for_each_set_bit(i, &tmp, 7) { + int sub_status, sub_mask; + int sub_status_reg[] = { + INT1_STATUS, INT2_STATUS, INT3_STATUS, INT4_STATUS, + INT5_STATUS, INT6_STATUS, INT7_STATUS, + }; + struct regmap_field *sub_mask_f[] = { + bd->rmap_fields[F_INT1_SET], + bd->rmap_fields[F_INT2_SET], + bd->rmap_fields[F_INT3_SET], + bd->rmap_fields[F_INT4_SET], + bd->rmap_fields[F_INT5_SET], + bd->rmap_fields[F_INT6_SET], + bd->rmap_fields[F_INT7_SET], + }; + + /* Clear sub IRQs */ + ret = regmap_read(bd->rmap, sub_status_reg[i], &sub_status); + if (ret) { + dev_err(bd->dev, "Failed to read IRQ sub-status\n"); + goto err_umask; + } + + ret = regmap_field_read(sub_mask_f[i], &sub_mask); + if (ret) { + dev_err(bd->dev, "Failed to read IRQ sub-mask\n"); + goto err_umask; + } + + /* Ack active sub-statuses */ + sub_status &= sub_mask; + + ret = regmap_write(bd->rmap, sub_status_reg[i], sub_status); + if (ret) { + dev_err(bd->dev, "Failed to ack sub-IRQ\n"); + goto err_umask; + } + } + + ret = regmap_field_write(bd->rmap_fields[F_INT0_SET], mask); + if (ret) + /* May as well retry once */ + goto err_umask; + + /* Read whole chip state */ + ret = bd9995x_get_chip_state(bd, &state); + if (ret < 0) { + dev_err(bd->dev, "Failed to read chip state\n"); + } else { + mutex_lock(&bd->lock); + bd->state = state; + mutex_unlock(&bd->lock); + + power_supply_changed(bd->charger); + } + + return IRQ_HANDLED; + +err_umask: + ret = regmap_field_write(bd->rmap_fields[F_INT0_SET], mask); + if (ret) + dev_err(bd->dev, + "Failed to un-mask F_INT0 - IRQ permanently disabled\n"); + + return IRQ_NONE; +} + +static int __bd9995x_chip_reset(struct bd9995x_device *bd) +{ + int ret, state; + int rst_check_counter = 10; + u16 tmp = ALLRST | OTPLD; + + ret = regmap_raw_write(bd->rmap, SYSTEM_CTRL_SET, &tmp, 2); + if (ret < 0) + return ret; + + do { + ret = regmap_field_read(bd->rmap_fields[F_OTPLD_STATE], &state); + if (ret) + return ret; + + msleep(10); + } while (state == 0 && --rst_check_counter); + + if (!rst_check_counter) { + dev_err(bd->dev, "chip reset not completed\n"); + return -ETIMEDOUT; + } + + tmp = 0; + ret = regmap_raw_write(bd->rmap, SYSTEM_CTRL_SET, &tmp, 2); + + return ret; +} + +static int bd9995x_hw_init(struct bd9995x_device *bd) +{ + int ret; + int i; + struct bd9995x_state state; + struct bd9995x_init_data *id = &bd->init_data; + + const struct { + enum bd9995x_fields id; + u16 value; + } init_data[] = { + /* Enable the charging trigger after SDP charger attached */ + {F_SDP_CHG_TRIG_EN, 1}, + /* Enable charging trigger after SDP charger attached */ + {F_SDP_CHG_TRIG, 1}, + /* Disable charging trigger by BC1.2 detection */ + {F_VBUS_BC_DISEN, 1}, + /* Disable charging trigger by BC1.2 detection */ + {F_VCC_BC_DISEN, 1}, + /* Disable automatic limitation of the input current */ + {F_ILIM_AUTO_DISEN, 1}, + /* Select current limitation when SDP charger attached*/ + {F_SDP_500_SEL, 1}, + /* Select current limitation when DCP charger attached */ + {F_DCP_2500_SEL, 1}, + {F_VSYSREG_SET, id->vsysreg_set}, + /* Activate USB charging and DC/DC converter */ + {F_USB_SUS, 0}, + /* DCDC clock: 1200 kHz*/ + {F_DCDC_CLK_SEL, 3}, + /* Enable charging */ + {F_CHG_EN, 1}, + /* Disable Input current Limit setting voltage measurement */ + {F_EXTIADPEN, 0}, + /* Disable input current limiting */ + {F_VSYS_PRIORITY, 1}, + {F_IBUS_LIM_SET, id->ibus_lim_set}, + {F_ICC_LIM_SET, id->icc_lim_set}, + /* Charge Termination Current Setting to 0*/ + {F_ITERM_SET, id->iterm_set}, + /* Trickle-charge Current Setting */ + {F_ITRICH_SET, id->itrich_set}, + /* Pre-charge Current setting */ + {F_IPRECH_SET, id->iprech_set}, + /* Fast Charge Current for constant current phase */ + {F_ICHG_SET, id->ichg_set}, + /* Fast Charge Voltage Regulation Setting */ + {F_VFASTCHG_REG_SET1, id->vfastchg_reg_set1}, + /* Set Pre-charge Voltage Threshold for trickle charging. */ + {F_VPRECHG_TH_SET, id->vprechg_th_set}, + {F_VRECHG_SET, id->vrechg_set}, + {F_VBATOVP_SET, id->vbatovp_set}, + /* Reverse buck boost voltage Setting */ + {F_VRBOOST_SET, 0}, + /* Disable fast-charging watchdog */ + {F_WDT_FST, 0}, + /* Disable pre-charging watchdog */ + {F_WDT_PRE, 0}, + /* Power save off */ + {F_POWER_SAVE_MODE, 0}, + {F_INT1_SET, INT1_ALL}, + {F_INT2_SET, INT2_ALL}, + {F_INT3_SET, INT3_ALL}, + {F_INT4_SET, INT4_ALL}, + {F_INT5_SET, INT5_ALL}, + {F_INT6_SET, INT6_ALL}, + {F_INT7_SET, INT7_ALL}, + }; + + /* + * Currently we initialize charger to a known state at startup. + * If we want to allow for example the boot code to initialize + * charger we should get rid of this. + */ + ret = __bd9995x_chip_reset(bd); + if (ret < 0) + return ret; + + /* Initialize currents/voltages and other parameters */ + for (i = 0; i < ARRAY_SIZE(init_data); i++) { + ret = regmap_field_write(bd->rmap_fields[init_data[i].id], + init_data[i].value); + if (ret) { + dev_err(bd->dev, "failed to initialize charger (%d)\n", + ret); + return ret; + } + } + + ret = bd9995x_get_chip_state(bd, &state); + if (ret < 0) + return ret; + + mutex_lock(&bd->lock); + bd->state = state; + mutex_unlock(&bd->lock); + + return 0; +} + +static enum power_supply_property bd9995x_power_supply_props[] = { + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CHARGE_AVG, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + /* Battery props we access through charger */ + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +static const struct power_supply_desc bd9995x_power_supply_desc = { + .name = "bd9995x-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = bd9995x_power_supply_props, + .num_properties = ARRAY_SIZE(bd9995x_power_supply_props), + .get_property = bd9995x_power_supply_get_property, +}; + +/* + * Limit configurations for vbus-input-current and vcc-vacp-input-current + * Minimum limit is 0 uA. Max is 511 * 32000 uA = 16352000 uA. This is + * configured by writing a register so that each increment in register + * value equals to 32000 uA limit increment. + * + * Eg, value 0x0 is limit 0, value 0x1 is limit 32000, ... + * Describe the setting in linear_range table. + */ +static const struct linear_range input_current_limit_ranges[] = { + { + .min = 0, + .step = 32000, + .min_sel = 0x0, + .max_sel = 0x1ff, + }, +}; + +/* Possible trickle, pre-charging and termination current values */ +static const struct linear_range charging_current_ranges[] = { + { + .min = 0, + .step = 64000, + .min_sel = 0x0, + .max_sel = 0x10, + }, { + .min = 1024000, + .step = 0, + .min_sel = 0x11, + .max_sel = 0x1f, + }, +}; + +/* + * Fast charging voltage regulation, starting re-charging limit + * and battery over voltage protection have same possible values + */ +static const struct linear_range charge_voltage_regulation_ranges[] = { + { + .min = 2560000, + .step = 0, + .min_sel = 0, + .max_sel = 0xA0, + }, { + .min = 2560000, + .step = 16000, + .min_sel = 0xA0, + .max_sel = 0x4B0, + }, { + .min = 19200000, + .step = 0, + .min_sel = 0x4B0, + .max_sel = 0x7FF, + }, +}; + +/* Possible VSYS voltage regulation values */ +static const struct linear_range vsys_voltage_regulation_ranges[] = { + { + .min = 2560000, + .step = 0, + .min_sel = 0, + .max_sel = 0x28, + }, { + .min = 2560000, + .step = 64000, + .min_sel = 0x28, + .max_sel = 0x12C, + }, { + .min = 19200000, + .step = 0, + .min_sel = 0x12C, + .max_sel = 0x1FF, + }, +}; + +/* Possible settings for switching from trickle to pre-charging limits */ +static const struct linear_range trickle_to_pre_threshold_ranges[] = { + { + .min = 2048000, + .step = 0, + .min_sel = 0, + .max_sel = 0x20, + }, { + .min = 2048000, + .step = 64000, + .min_sel = 0x20, + .max_sel = 0x12C, + }, { + .min = 19200000, + .step = 0, + .min_sel = 0x12C, + .max_sel = 0x1FF + } +}; + +/* Possible current values for fast-charging constant current phase */ +static const struct linear_range fast_charge_current_ranges[] = { + { + .min = 0, + .step = 64000, + .min_sel = 0, + .max_sel = 0xFF, + } +}; + +struct battery_init { + const char *name; + int *info_data; + const struct linear_range *range; + int ranges; + u16 *data; +}; + +struct dt_init { + char *prop; + const struct linear_range *range; + int ranges; + u16 *data; +}; + +static int bd9995x_fw_probe(struct bd9995x_device *bd) +{ + int ret; + struct power_supply_battery_info info; + u32 property; + int i; + int regval; + bool found; + struct bd9995x_init_data *init = &bd->init_data; + struct battery_init battery_inits[] = { + { + .name = "trickle-charging current", + .info_data = &info.tricklecharge_current_ua, + .range = &charging_current_ranges[0], + .ranges = 2, + .data = &init->itrich_set, + }, { + .name = "pre-charging current", + .info_data = &info.precharge_current_ua, + .range = &charging_current_ranges[0], + .ranges = 2, + .data = &init->iprech_set, + }, { + .name = "pre-to-trickle charge voltage threshold", + .info_data = &info.precharge_voltage_max_uv, + .range = &trickle_to_pre_threshold_ranges[0], + .ranges = 2, + .data = &init->vprechg_th_set, + }, { + .name = "charging termination current", + .info_data = &info.charge_term_current_ua, + .range = &charging_current_ranges[0], + .ranges = 2, + .data = &init->iterm_set, + }, { + .name = "charging re-start voltage", + .info_data = &info.charge_restart_voltage_uv, + .range = &charge_voltage_regulation_ranges[0], + .ranges = 2, + .data = &init->vrechg_set, + }, { + .name = "battery overvoltage limit", + .info_data = &info.overvoltage_limit_uv, + .range = &charge_voltage_regulation_ranges[0], + .ranges = 2, + .data = &init->vbatovp_set, + }, { + .name = "fast-charging max current", + .info_data = &info.constant_charge_current_max_ua, + .range = &fast_charge_current_ranges[0], + .ranges = 1, + .data = &init->ichg_set, + }, { + .name = "fast-charging voltage", + .info_data = &info.constant_charge_voltage_max_uv, + .range = &charge_voltage_regulation_ranges[0], + .ranges = 2, + .data = &init->vfastchg_reg_set1, + }, + }; + struct dt_init props[] = { + { + .prop = "rohm,vsys-regulation-microvolt", + .range = &vsys_voltage_regulation_ranges[0], + .ranges = 2, + .data = &init->vsysreg_set, + }, { + .prop = "rohm,vbus-input-current-limit-microamp", + .range = &input_current_limit_ranges[0], + .ranges = 1, + .data = &init->ibus_lim_set, + }, { + .prop = "rohm,vcc-input-current-limit-microamp", + .range = &input_current_limit_ranges[0], + .ranges = 1, + .data = &init->icc_lim_set, + }, + }; + + /* + * The power_supply_get_battery_info() does not support getting values + * from ACPI. Let's fix it if ACPI is required here. + */ + ret = power_supply_get_battery_info(bd->charger, &info); + if (ret < 0) + return ret; + + for (i = 0; i < ARRAY_SIZE(battery_inits); i++) { + int val = *battery_inits[i].info_data; + const struct linear_range *range = battery_inits[i].range; + int ranges = battery_inits[i].ranges; + + if (val == -EINVAL) + continue; + + ret = linear_range_get_selector_low_array(range, ranges, val, + ®val, &found); + if (ret) { + dev_err(bd->dev, "Unsupported value for %s\n", + battery_inits[i].name); + + power_supply_put_battery_info(bd->charger, &info); + return -EINVAL; + } + if (!found) { + dev_warn(bd->dev, + "Unsupported value for %s - using smaller\n", + battery_inits[i].name); + } + *(battery_inits[i].data) = regval; + } + + power_supply_put_battery_info(bd->charger, &info); + + for (i = 0; i < ARRAY_SIZE(props); i++) { + ret = device_property_read_u32(bd->dev, props[i].prop, + &property); + if (ret < 0) { + dev_err(bd->dev, "failed to read %s", props[i].prop); + + return ret; + } + + ret = linear_range_get_selector_low_array(props[i].range, + props[i].ranges, + property, ®val, + &found); + if (ret) { + dev_err(bd->dev, "Unsupported value for '%s'\n", + props[i].prop); + + return -EINVAL; + } + + if (!found) { + dev_warn(bd->dev, + "Unsupported value for '%s' - using smaller\n", + props[i].prop); + } + + *(props[i].data) = regval; + } + + return 0; +} + +static void bd9995x_chip_reset(void *bd) +{ + __bd9995x_chip_reset(bd); +} + +static int bd9995x_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct bd9995x_device *bd; + struct power_supply_config psy_cfg = {}; + int ret; + int i; + + bd = devm_kzalloc(dev, sizeof(*bd), GFP_KERNEL); + if (!bd) + return -ENOMEM; + + bd->client = client; + bd->dev = dev; + psy_cfg.drv_data = bd; + psy_cfg.of_node = dev->of_node; + + mutex_init(&bd->lock); + + bd->rmap = devm_regmap_init_i2c(client, &bd9995x_regmap_config); + if (IS_ERR(bd->rmap)) { + dev_err(dev, "Failed to setup register access via i2c\n"); + return PTR_ERR(bd->rmap); + } + + for (i = 0; i < ARRAY_SIZE(bd9995x_reg_fields); i++) { + const struct reg_field *reg_fields = bd9995x_reg_fields; + + bd->rmap_fields[i] = devm_regmap_field_alloc(dev, bd->rmap, + reg_fields[i]); + if (IS_ERR(bd->rmap_fields[i])) { + dev_err(dev, "cannot allocate regmap field\n"); + return PTR_ERR(bd->rmap_fields[i]); + } + } + + i2c_set_clientdata(client, bd); + + ret = regmap_field_read(bd->rmap_fields[F_CHIP_ID], &bd->chip_id); + if (ret) { + dev_err(dev, "Cannot read chip ID.\n"); + return ret; + } + + if (bd->chip_id != BD99954_ID) { + dev_err(dev, "Chip with ID=0x%x, not supported!\n", + bd->chip_id); + return -ENODEV; + } + + ret = regmap_field_read(bd->rmap_fields[F_CHIP_REV], &bd->chip_rev); + if (ret) { + dev_err(dev, "Cannot read revision.\n"); + return ret; + } + + dev_info(bd->dev, "Found BD99954 chip rev %d\n", bd->chip_rev); + + /* + * We need to init the psy before we can call + * power_supply_get_battery_info() for it + */ + bd->charger = devm_power_supply_register(bd->dev, + &bd9995x_power_supply_desc, + &psy_cfg); + if (IS_ERR(bd->charger)) { + dev_err(dev, "Failed to register power supply\n"); + return PTR_ERR(bd->charger); + } + + ret = bd9995x_fw_probe(bd); + if (ret < 0) { + dev_err(dev, "Cannot read device properties.\n"); + return ret; + } + + ret = bd9995x_hw_init(bd); + if (ret < 0) { + dev_err(dev, "Cannot initialize the chip.\n"); + return ret; + } + + ret = devm_add_action_or_reset(dev, bd9995x_chip_reset, bd); + if (ret) + return ret; + + return devm_request_threaded_irq(dev, client->irq, NULL, + bd9995x_irq_handler_thread, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + BD9995X_IRQ_PIN, bd); +} + +static const struct of_device_id bd9995x_of_match[] = { + { .compatible = "rohm,bd99954", }, + { } +}; +MODULE_DEVICE_TABLE(of, bd9995x_of_match); + +static struct i2c_driver bd9995x_driver = { + .driver = { + .name = "bd9995x-charger", + .of_match_table = bd9995x_of_match, + }, + .probe_new = bd9995x_probe, +}; +module_i2c_driver(bd9995x_driver); + +MODULE_AUTHOR("Laine Markus <markus.laine@fi.rohmeurope.com>"); +MODULE_DESCRIPTION("ROHM BD99954 charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bd99954-charger.h b/drivers/power/supply/bd99954-charger.h new file mode 100644 index 000000000..f58897925 --- /dev/null +++ b/drivers/power/supply/bd99954-charger.h @@ -0,0 +1,1075 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright (C) 2020 ROHM Semiconductors */ +#ifndef BD99954_CHARGER_H +#define BD99954_CHARGER_H + +#include <linux/regmap.h> + +#define BD9995X_MANUFACTURER "Rohm Semiconductor" +#define BD9995X_IRQ_PIN "bd9995x_irq" + +#define BD9995X_VSYS_PRECHARGE_OFFSET_MV 200 + +#define BD99954_ID 0x346 +#define BD99955_ID 0x221 +#define BD99956_ID 0x331 + +/* Battery Charger Commands */ +#define CHARGING_CURRENT 0x14 +#define CHARGING_VOLTAGE 0x15 +#define PROTECT_SET 0x3E +#define MAP_SET 0x3F + +/* Extended commands */ +#define CHGSTM_STATUS 0x100 +#define VBAT_VSYS_STATUS 0x101 +#define VBUS_VCC_STATUS 0x102 +#define CHGOP_STATUS 0x103 +#define WDT_STATUS 0x104 +#define CUR_ILIM_VAL 0x105 +#define SEL_ILIM_VAL 0x106 +#define IBUS_LIM_SET 0x107 +#define ICC_LIM_SET 0x108 +#define IOTG_LIM_SET 0x109 +#define VIN_CTRL_SET 0x10A +#define CHGOP_SET1 0x10B +#define CHGOP_SET2 0x10C +#define VBUSCLPS_TH_SET 0x10D +#define VCCCLPS_TH_SET 0x10E +#define CHGWDT_SET 0x10F +#define BATTWDT_SET 0x110 +#define VSYSREG_SET 0x111 +#define VSYSVAL_THH_SET 0x112 +#define VSYSVAL_THL_SET 0x113 +#define ITRICH_SET 0x114 +#define IPRECH_SET 0x115 +#define ICHG_SET 0x116 +#define ITERM_SET 0x117 +#define VPRECHG_TH_SET 0x118 +#define VRBOOST_SET 0x119 +#define VFASTCHG_REG_SET1 0x11A +#define VFASTCHG_REG_SET2 0x11B +#define VFASTCHG_REG_SET3 0x11C +#define VRECHG_SET 0x11D +#define VBATOVP_SET 0x11E +#define IBATSHORT_SET 0x11F +#define PROCHOT_CTRL_SET 0x120 +#define PROCHOT_ICRIT_SET 0x121 +#define PROCHOT_INORM_SET 0x122 +#define PROCHOT_IDCHG_SET 0x123 +#define PROCHOT_VSYS_SET 0x124 +#define PMON_IOUT_CTRL_SET 0x125 +#define PMON_DACIN_VAL 0x126 +#define IOUT_DACIN_VAL 0x127 +#define VCC_UCD_SET 0x128 +#define VCC_UCD_STATUS 0x129 +#define VCC_IDD_STATUS 0x12A +#define VCC_UCD_FCTRL_SET 0x12B +#define VCC_UCD_FCTRL_EN 0x12C +#define VBUS_UCD_SET 0x130 +#define VBUS_UCD_STATUS 0x131 +#define VBUS_IDD_STATUS 0x132 +#define VBUS_UCD_FCTRL_SET 0x133 +#define VBUS_UCD_FCTRL_EN 0x134 +#define CHIP_ID 0x138 +#define CHIP_REV 0x139 +#define IC_SET1 0x13A +#define IC_SET2 0x13B +#define SYSTEM_STATUS 0x13C +#define SYSTEM_CTRL_SET 0x13D +#define VM_CTRL_SET 0x140 +#define THERM_WINDOW_SET1 0x141 +#define THERM_WINDOW_SET2 0x142 +#define THERM_WINDOW_SET3 0x143 +#define THERM_WINDOW_SET4 0x144 +#define THERM_WINDOW_SET5 0x145 +#define IBATP_TH_SET 0x146 +#define IBATM_TH_SET 0x147 +#define VBAT_TH_SET 0x148 +#define THERM_TH_SET 0x149 +#define IACP_TH_SET 0x14A +#define VACP_TH_SET 0x14B +#define VBUS_TH_SET 0x14C +#define VCC_TH_SET 0x14D +#define VSYS_TH_SET 0x14E +#define EXTIADP_TH_SET 0x14F +#define IBATP_VAL 0x150 +#define IBATP_AVE_VAL 0x151 +#define IBATM_VAL 0x152 +#define IBATM_AVE_VAL 0x153 +#define VBAT_VAL 0x154 +#define VBAT_AVE_VAL 0x155 +#define THERM_VAL 0x156 +#define VTH_VAL 0x157 +#define IACP_VAL 0x158 +#define IACP_AVE_VAL 0x159 +#define VACP_VAL 0x15A +#define VACP_AVE_VAL 0x15B +#define VBUS_VAL 0x15C +#define VBUS_AVE_VAL 0x15D +#define VCC_VAL 0x15E +#define VCC_AVE_VAL 0x15F +#define VSYS_VAL 0x160 +#define VSYS_AVE_VAL 0x161 +#define EXTIADP_VAL 0x162 +#define EXTIADP_AVE_VAL 0x163 +#define VACPCLPS_TH_SET 0x164 +#define INT0_SET 0x168 +#define INT1_SET 0x169 +#define INT2_SET 0x16A +#define INT3_SET 0x16B +#define INT4_SET 0x16C +#define INT5_SET 0x16D +#define INT6_SET 0x16E +#define INT7_SET 0x16F +#define INT0_STATUS 0x170 +#define INT1_STATUS 0x171 +#define INT2_STATUS 0x172 +#define INT3_STATUS 0x173 +#define INT4_STATUS 0x174 +#define INT5_STATUS 0x175 +#define INT6_STATUS 0x176 +#define INT7_STATUS 0x177 +#define OTPREG0 0x17A +#define OTPREG1 0x17B +#define SMBREG 0x17C +#define DEBUG_MODE_SET 0x17F +#define DEBUG0x14 0x214 +#define DEBUG0x1A 0x21A + +enum bd9995x_fields { + F_PREV_CHGSTM_STATE, F_CHGSTM_STATE, + F_VBAT_VSYS_STATUS, + F_VBUS_VCC_STATUS, + F_BATTEMP, F_VRECHG_DET, F_RBOOST_UV, F_RBOOSTS, + F_THERMWDT_VAL, F_CHGWDT_VAL, + F_CUR_ILIM_VAL, + F_SEL_ILIM_VAL, + F_IBUS_LIM_SET, + F_ICC_LIM_SET, + F_IOTG_LIM_SET, + F_OTG_BOTH_EN, + F_VRBOOST_TRIG, + F_VRBOOST_EN, + F_PP_BOTH_THRU, + F_VIN_ORD, + F_VBUS_EN, + F_VCC_EN, + F_VSYS_PRIORITY, + F_PPC_SUB_CAP, + F_PPC_CAP, + F_DCP_2500_SEL, + F_SDP_500_SEL, + F_ILIM_AUTO_DISEN, + F_VCC_BC_DISEN, + F_VBUS_BC_DISEN, + F_SDP_CHG_TRIG_EN, + F_SDP_CHG_TRIG, + F_AUTO_TOF, + F_AUTO_FST, + F_AUTO_RECH, + F_ILIM_RESET_EN, + F_DCDC_1MS_SEL, + F_SEL_ILIM_DIV, + F_BATT_LEARN, + F_CHG_EN, + F_USB_SUS, + F_CHOP_SS_INIT, + F_CHOP_ALL_INIT, + F_DCDC_CLK_SEL, + F_CHOP_SS, + F_CHOP_ALL, + F_VBUSCLPS_TH_SET, + F_VCCCLPS_TH_SET, + F_WDT_FST, + F_WDT_PRE, + F_WDT_IBAT_SHORT, + F_WDT_THERM, + F_VSYSREG_SET, + F_VSYSVAL_THH_SET, + F_VSYSVAL_THL_SET, + F_ITRICH_SET, + F_IPRECH_SET, + F_ICHG_SET, + F_ITERM_SET, + F_VPRECHG_TH_SET, + F_VRBOOST_SET, + F_VFASTCHG_REG_SET1, + F_VFASTCHG_REG_SET2, + F_VFASTCHG_REG_SET3, + F_VRECHG_SET, + F_VBATOVP_SET, + F_IBATM_SHORT_SET, + F_PROCHOT_DG_SET, + F_PROCHOT_ICRIT_DG_SET, + F_PROCHOT_IDCHG_DG_SET, + F_PROCHOT_EN, + F_PROCHOT_ICRIT_SET, + F_PROCHOT_INORM_SET, + F_PROCHOT_IDCHG_SET, + F_PROCHOT_VSYS_SET, + F_IMON_INSEL, + F_PMON_INSEL, + F_IOUT_OUT_EN, + F_IOUT_SOURCE_SEL, + F_IOUT_GAIN_SET, + F_PMON_OUT_EN, + F_PMON_GAIN_SET, + F_PMON_DACIN_VAL, + F_IOUT_DACIN_VAL, + F_VCC_BCSRETRY, + F_VCC_ADCRTRY, + F_VCC_USBDETEN, + F_VCC_IDRDETEN, + F_VCC_ENUMRDY, + F_VCC_ADCPOLEN, + F_VCC_DCDMODE, + F_VCC_USB_SW_EN, + F_VCC_USB_SW, + F_VCC_DCDFAIL, + F_VCC_CHGPORT, + F_VCC_PUPDET, + F_VCC_VBUS_VLD, + F_VCC_CHGDET, + F_VCC_OTGDET, + F_VCC_VBINOP, + F_VCC_EXTID, + F_VCC_IDRDET, + F_VCC_INDO, + F_VCC_UCDSWEN, + F_VCC_RREF_EN, + F_VCC_DPPU_EN, + F_VCC_DPREF_EN, + F_VCC_DMREF_EN, + F_VCC_DPDET_EN, + F_VCC_DMDET_EN, + F_VCC_DPSINK_EN, + F_VCC_DMSINK_EN, + F_VCC_DP_BUFF_EN, + F_VCC_DM_BUFF_EN, + F_VCC_EXTCLKENBL, + F_VCC_PLSTESTEN, + F_VCC_UCDSWEN_TSTENB, + F_VCC_RREF_EN_TSTENB, + F_VCC_DPPU_EN_TSTENB, + F_VCC_DPREF_EN_TSTENB, + F_VCC_DMREF_EN_TSTENB, + F_VCC_DPDET_EN_TSTENB, + F_VCC_DMDET_EN_TSTENB, + F_VCC_DPSINK_EN_TSTENB, + F_VCC_DMSINK_EN_TSTENB, + F_VCC_DP_BUFF_EN_TSTENB, + F_VCC_DM_BUFF_EN_TSTENB, + F_VBUS_BCSRETRY, + F_VBUS_ADCRTRY, + F_VBUS_USBDETEN, + F_VBUS_IDRDETEN, + F_VBUS_ENUMRDY, + F_VBUS_ADCPOLEN, + F_VBUS_DCDMODE, + F_VBUS_USB_SW_EN, + F_VBUS_USB_SW, + F_VBUS_DCDFAIL, + F_VBUS_CHGPORT, + F_VBUS_PUPDET, + F_VBUS_VBUS_VLD, + F_VBUS_CHGDET, + F_VBUS_OTGDET, + F_VBUS_VBINOP, + F_VBUS_EXTID, + F_VBUS_IDRDET, + F_VBUS_INDO, + F_VBUS_UCDSWEN, + F_VBUS_RREF_EN, + F_VBUS_DPPU_EN, + F_VBUS_DPREF_EN, + F_VBUS_DMREF_EN, + F_VBUS_DPDET_EN, + F_VBUS_DMDET_EN, + F_VBUS_DPSINK_EN, + F_VBUS_DMSINK_EN, + F_VBUS_DP_BUFF_EN, + F_VBUS_DM_BUFF_EN, + F_VBUS_EXTCLKENBL, + F_VBUS_PLSTESTEN, + F_VBUS_UCDSWEN_TSTENB, + F_VBUS_RREF_EN_TSTENB, + F_VBUS_DPPU_EN_TSTENB, + F_VBUS_DPREF_EN_TSTENB, + F_VBUS_DMREF_EN_TSTENB, + F_VBUS_DPDET_EN_TSTENB, + F_VBUS_DMDET_EN_TSTENB, + F_VBUS_DPSINK_EN_TSTENB, + F_VBUS_DMSINK_EN_TSTENB, + F_VBUS_DP_BUFF_EN_TSTENB, + F_VBUS_DM_BUFF_EN_TSTENB, + F_CHIP_ID, + F_CHIP_REV, + F_ONE_CELL_MODE, + F_cell, + F_VACP_AUTO_DISCHG, + F_VACP_LOAD, + F_ACOK_POL, + F_ACOK_DISEN, + F_DEBUG_SET1, + F_DEBUG_SET0, + F_MONRST_STATE, + F_ALMRST_STATE, + F_CHGRST_STATE, + F_OTPLD_STATE, + F_ALLRST_STATE, + F_PROTECT_SET, + F_MAP_SET, + F_ADCINTERVAL, + F_ADCMOD, + F_ADCTMOD, + F_EXTIADPEN, + F_VSYSENB, + F_VCCENB, + F_VBUSENB, + F_VACPENB, + F_IACPENB, + F_THERMENB, + F_VBATENB, + F_IBATMENB, + F_IBATPENB, + F_TMPTHR1B, + F_TMPTHR1A, + F_TMPTHR2B, + F_TMPTHR2A, + F_TMPTHR3B, + F_TMPTHR3A, + F_TMPTHR4B, + F_TMPTHR4A, + F_TMPTHR5B, + F_TMPTHR5A, + F_IBATP_TH_SET, + F_IBATM_TH_SET, + F_VBAT_TH_SET, + F_THERM_TH_SET, + F_IACP_TH_SET, + F_VACP_TH_SET, + F_VBUS_TH_SET, + F_VCC_TH_SET, + F_VSYS_TH_SET, + F_EXTIADP_TH_SET, + F_IBATP_VAL, + F_IBATP_AVE_VAL, + F_IBATM_VAL, + F_IBATM_AVE_VAL, + F_VBAT_VAL, + F_VBAT_AVE_VAL, + F_THERM_VAL, + F_VTH_VAL, + F_IACP_VAL, + F_IACP_AVE_VAL, + F_VACP_VAL, + F_VACP_AVE_VAL, + F_VBUS_VAL, + F_VBUS_AVE_VAL, + F_VCC_VAL, + F_VCC_AVE_VAL, + F_VSYS_VAL, + F_VSYS_AVE_VAL, + F_EXTIADP_VAL, + F_EXTIADP_AVE_VAL, + F_VACPCLPS_TH_SET, + F_INT7_SET, + F_INT6_SET, + F_INT5_SET, + F_INT4_SET, + F_INT3_SET, + F_INT2_SET, + F_INT1_SET, + F_INT0_SET, + F_VBUS_RBUV_DET, + F_VBUS_RBUV_RES, + F_VBUS_TH_DET, + F_VBUS_TH_RES, + F_VBUS_IIN_MOD, + F_VBUS_OV_DET, + F_VBUS_OV_RES, + F_VBUS_CLPS_DET, + F_VBUS_CLPS, + F_VBUS_DET, + F_VBUS_RES, + F_VCC_RBUV_DET, + F_VCC_RBUV_RES, + F_VCC_TH_DET, + F_VCC_TH_RES, + F_VCC_IIN_MOD, + F_VCC_OVP_DET, + F_VCC_OVP_RES, + F_VCC_CLPS_DET, + F_VCC_CLPS_RES, + F_VCC_DET, + F_VCC_RES, + F_TH_DET, + F_TH_RMV, + F_TMP_OUT_DET, + F_TMP_OUT_RES, + F_VBAT_TH_DET, + F_VBAT_TH_RES, + F_IBAT_SHORT_DET, + F_IBAT_SHORT_RES, + F_VBAT_OV_DET, + F_VBAT_OV_RES, + F_BAT_ASSIST_DET, + F_BAT_ASSIST_RES, + F_VSYS_TH_DET, + F_VSYS_TH_RES, + F_VSYS_OV_DET, + F_VSYS_OV_RES, + F_VSYS_SHT_DET, + F_VSYS_SHT_RES, + F_VSYS_UV_DET, + F_VSYS_UV_RES, + F_OTP_LOAD_DONE, + F_PWR_ON, + F_EXTIADP_TRNS, + F_EXTIADP_TH_DET, + F_EXIADP_TH_RES, + F_BAT_MNT_DET, + F_BAT_MNT_RES, + F_TSD_DET, + F_TSD_RES, + F_CHGWDT_EXP, + F_THERMWDT_EXP, + F_TMP_TRNS, + F_CHG_TRNS, + F_VBUS_UCD_PORT_DET, + F_VBUS_UCD_UCHG_DET, + F_VBUS_UCD_URID_RMV, + F_VBUS_UCD_OTG_DET, + F_VBUS_UCD_URID_MOD, + F_VCC_UCD_PORT_DET, + F_VCC_UCD_UCHG_DET, + F_VCC_UCD_URID_RMV, + F_VCC_UCD_OTG_DET, + F_VCC_UCD_URID_MOD, + F_PROCHOT_DET, + F_PROCHOT_RES, + F_VACP_DET, + F_VACP_RES, + F_VACP_TH_DET, + F_VACP_TH_RES, + F_IACP_TH_DET, + F_IACP_THE_RES, + F_THERM_TH_DET, + F_THERM_TH_RES, + F_IBATM_TH_DET, + F_IBATM_TH_RES, + F_IBATP_TH_DET, + F_IBATP_TH_RES, + F_INT7_STATUS, + F_INT6_STATUS, + F_INT5_STATUS, + F_INT4_STATUS, + F_INT3_STATUS, + F_INT2_STATUS, + F_INT1_STATUS, + F_INT0_STATUS, + F_ILIM_DECREASE, + F_RESERVE_OTPREG1, + F_POWER_SAVE_MODE, + F_DEBUG_MODE_SET, + F_DEBUG0x14, + F_DEBUG0x1A, + F_MAX_FIELDS +}; + +static const struct reg_field bd9995x_reg_fields[] = { + [F_PREV_CHGSTM_STATE] = REG_FIELD(CHGSTM_STATUS, 8, 14), + [F_CHGSTM_STATE] = REG_FIELD(CHGSTM_STATUS, 0, 6), + [F_VBAT_VSYS_STATUS] = REG_FIELD(VBAT_VSYS_STATUS, 0, 15), + [F_VBUS_VCC_STATUS] = REG_FIELD(VBUS_VCC_STATUS, 0, 12), + [F_BATTEMP] = REG_FIELD(CHGOP_STATUS, 8, 10), + [F_VRECHG_DET] = REG_FIELD(CHGOP_STATUS, 6, 6), + [F_RBOOST_UV] = REG_FIELD(CHGOP_STATUS, 1, 1), + [F_RBOOSTS] = REG_FIELD(CHGOP_STATUS, 0, 0), + [F_THERMWDT_VAL] = REG_FIELD(WDT_STATUS, 8, 15), + [F_CHGWDT_VAL] = REG_FIELD(WDT_STATUS, 0, 7), + [F_CUR_ILIM_VAL] = REG_FIELD(CUR_ILIM_VAL, 0, 13), + [F_SEL_ILIM_VAL] = REG_FIELD(SEL_ILIM_VAL, 0, 13), + [F_IBUS_LIM_SET] = REG_FIELD(IBUS_LIM_SET, 5, 13), + [F_ICC_LIM_SET] = REG_FIELD(ICC_LIM_SET, 5, 13), + [F_IOTG_LIM_SET] = REG_FIELD(IOTG_LIM_SET, 5, 13), + [F_OTG_BOTH_EN] = REG_FIELD(VIN_CTRL_SET, 15, 15), + [F_VRBOOST_TRIG] = REG_FIELD(VIN_CTRL_SET, 14, 14), + [F_VRBOOST_EN] = REG_FIELD(VIN_CTRL_SET, 12, 13), + [F_PP_BOTH_THRU] = REG_FIELD(VIN_CTRL_SET, 11, 11), + [F_VIN_ORD] = REG_FIELD(VIN_CTRL_SET, 7, 7), + [F_VBUS_EN] = REG_FIELD(VIN_CTRL_SET, 6, 6), + [F_VCC_EN] = REG_FIELD(VIN_CTRL_SET, 5, 5), + [F_VSYS_PRIORITY] = REG_FIELD(VIN_CTRL_SET, 4, 4), + [F_PPC_SUB_CAP] = REG_FIELD(VIN_CTRL_SET, 2, 3), + [F_PPC_CAP] = REG_FIELD(VIN_CTRL_SET, 0, 1), + [F_DCP_2500_SEL] = REG_FIELD(CHGOP_SET1, 15, 15), + [F_SDP_500_SEL] = REG_FIELD(CHGOP_SET1, 14, 14), + [F_ILIM_AUTO_DISEN] = REG_FIELD(CHGOP_SET1, 13, 13), + [F_VCC_BC_DISEN] = REG_FIELD(CHGOP_SET1, 11, 11), + [F_VBUS_BC_DISEN] = REG_FIELD(CHGOP_SET1, 10, 10), + [F_SDP_CHG_TRIG_EN] = REG_FIELD(CHGOP_SET1, 9, 9), + [F_SDP_CHG_TRIG] = REG_FIELD(CHGOP_SET1, 8, 8), + [F_AUTO_TOF] = REG_FIELD(CHGOP_SET1, 6, 6), + [F_AUTO_FST] = REG_FIELD(CHGOP_SET1, 5, 5), + [F_AUTO_RECH] = REG_FIELD(CHGOP_SET1, 3, 3), + [F_ILIM_RESET_EN] = REG_FIELD(CHGOP_SET2, 14, 14), + [F_DCDC_1MS_SEL] = REG_FIELD(CHGOP_SET2, 12, 13), + [F_SEL_ILIM_DIV] = REG_FIELD(CHGOP_SET2, 10, 10), + [F_BATT_LEARN] = REG_FIELD(CHGOP_SET2, 8, 8), + [F_CHG_EN] = REG_FIELD(CHGOP_SET2, 7, 7), + [F_USB_SUS] = REG_FIELD(CHGOP_SET2, 6, 6), + [F_CHOP_SS_INIT] = REG_FIELD(CHGOP_SET2, 5, 5), + [F_CHOP_ALL_INIT] = REG_FIELD(CHGOP_SET2, 4, 4), + [F_DCDC_CLK_SEL] = REG_FIELD(CHGOP_SET2, 2, 3), + [F_CHOP_SS] = REG_FIELD(CHGOP_SET2, 1, 1), + [F_CHOP_ALL] = REG_FIELD(CHGOP_SET2, 0, 0), + [F_VBUSCLPS_TH_SET] = REG_FIELD(VBUSCLPS_TH_SET, 7, 14), + [F_VCCCLPS_TH_SET] = REG_FIELD(VCCCLPS_TH_SET, 7, 14), + [F_WDT_FST] = REG_FIELD(CHGWDT_SET, 8, 15), + [F_WDT_PRE] = REG_FIELD(CHGWDT_SET, 0, 7), + [F_WDT_IBAT_SHORT] = REG_FIELD(BATTWDT_SET, 8, 15), + [F_WDT_THERM] = REG_FIELD(BATTWDT_SET, 0, 7), + [F_VSYSREG_SET] = REG_FIELD(VSYSREG_SET, 6, 14), + [F_VSYSVAL_THH_SET] = REG_FIELD(VSYSVAL_THH_SET, 6, 14), + [F_VSYSVAL_THL_SET] = REG_FIELD(VSYSVAL_THL_SET, 6, 14), + [F_ITRICH_SET] = REG_FIELD(ITRICH_SET, 6, 10), + [F_IPRECH_SET] = REG_FIELD(IPRECH_SET, 6, 10), + [F_ICHG_SET] = REG_FIELD(ICHG_SET, 6, 13), + [F_ITERM_SET] = REG_FIELD(ITERM_SET, 6, 10), + [F_VPRECHG_TH_SET] = REG_FIELD(VPRECHG_TH_SET, 6, 14), + [F_VRBOOST_SET] = REG_FIELD(VRBOOST_SET, 6, 14), + [F_VFASTCHG_REG_SET1] = REG_FIELD(VFASTCHG_REG_SET1, 4, 14), + [F_VFASTCHG_REG_SET2] = REG_FIELD(VFASTCHG_REG_SET2, 4, 14), + [F_VFASTCHG_REG_SET3] = REG_FIELD(VFASTCHG_REG_SET3, 4, 14), + [F_VRECHG_SET] = REG_FIELD(VRECHG_SET, 4, 14), + [F_VBATOVP_SET] = REG_FIELD(VBATOVP_SET, 4, 14), + [F_IBATM_SHORT_SET] = REG_FIELD(IBATSHORT_SET, 0, 14), + [F_PROCHOT_DG_SET] = REG_FIELD(PROCHOT_CTRL_SET, 14, 15), + [F_PROCHOT_ICRIT_DG_SET] = REG_FIELD(PROCHOT_CTRL_SET, 10, 11), + [F_PROCHOT_IDCHG_DG_SET] = REG_FIELD(PROCHOT_CTRL_SET, 8, 9), + [F_PROCHOT_EN] = REG_FIELD(PROCHOT_CTRL_SET, 0, 4), + [F_PROCHOT_ICRIT_SET] = REG_FIELD(PROCHOT_ICRIT_SET, 0, 14), + [F_PROCHOT_INORM_SET] = REG_FIELD(PROCHOT_INORM_SET, 0, 14), + [F_PROCHOT_IDCHG_SET] = REG_FIELD(PROCHOT_IDCHG_SET, 0, 14), + [F_PROCHOT_VSYS_SET] = REG_FIELD(PROCHOT_VSYS_SET, 0, 14), + [F_IMON_INSEL] = REG_FIELD(PMON_IOUT_CTRL_SET, 9, 9), + [F_PMON_INSEL] = REG_FIELD(PMON_IOUT_CTRL_SET, 8, 8), + [F_IOUT_OUT_EN] = REG_FIELD(PMON_IOUT_CTRL_SET, 7, 7), + [F_IOUT_SOURCE_SEL] = REG_FIELD(PMON_IOUT_CTRL_SET, 6, 6), + [F_IOUT_GAIN_SET] = REG_FIELD(PMON_IOUT_CTRL_SET, 4, 5), + [F_PMON_OUT_EN] = REG_FIELD(PMON_IOUT_CTRL_SET, 3, 3), + [F_PMON_GAIN_SET] = REG_FIELD(PMON_IOUT_CTRL_SET, 0, 2), + [F_PMON_DACIN_VAL] = REG_FIELD(PMON_DACIN_VAL, 0, 9), + [F_IOUT_DACIN_VAL] = REG_FIELD(IOUT_DACIN_VAL, 0, 11), + [F_VCC_BCSRETRY] = REG_FIELD(VCC_UCD_SET, 12, 12), + [F_VCC_ADCRTRY] = REG_FIELD(VCC_UCD_SET, 8, 8), + [F_VCC_USBDETEN] = REG_FIELD(VCC_UCD_SET, 7, 7), + [F_VCC_IDRDETEN] = REG_FIELD(VCC_UCD_SET, 6, 6), + [F_VCC_ENUMRDY] = REG_FIELD(VCC_UCD_SET, 5, 5), + [F_VCC_ADCPOLEN] = REG_FIELD(VCC_UCD_SET, 4, 4), + [F_VCC_DCDMODE] = REG_FIELD(VCC_UCD_SET, 3, 3), + [F_VCC_USB_SW_EN] = REG_FIELD(VCC_UCD_SET, 1, 1), + [F_VCC_USB_SW] = REG_FIELD(VCC_UCD_SET, 0, 0), + [F_VCC_DCDFAIL] = REG_FIELD(VCC_UCD_STATUS, 15, 15), + [F_VCC_CHGPORT] = REG_FIELD(VCC_UCD_STATUS, 12, 13), + [F_VCC_PUPDET] = REG_FIELD(VCC_UCD_STATUS, 11, 11), + [F_VCC_VBUS_VLD] = REG_FIELD(VCC_UCD_STATUS, 7, 7), + [F_VCC_CHGDET] = REG_FIELD(VCC_UCD_STATUS, 6, 6), + [F_VCC_OTGDET] = REG_FIELD(VCC_UCD_STATUS, 3, 3), + [F_VCC_VBINOP] = REG_FIELD(VCC_IDD_STATUS, 6, 6), + [F_VCC_EXTID] = REG_FIELD(VCC_IDD_STATUS, 5, 5), + [F_VCC_IDRDET] = REG_FIELD(VCC_IDD_STATUS, 4, 4), + [F_VCC_INDO] = REG_FIELD(VCC_IDD_STATUS, 0, 3), + [F_VCC_UCDSWEN] = REG_FIELD(VCC_UCD_FCTRL_SET, 10, 10), + [F_VCC_RREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 9, 9), + [F_VCC_DPPU_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 8, 8), + [F_VCC_DPREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 7, 7), + [F_VCC_DMREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 6, 6), + [F_VCC_DPDET_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 5, 5), + [F_VCC_DMDET_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 4, 4), + [F_VCC_DPSINK_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 3, 3), + [F_VCC_DMSINK_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 2, 2), + [F_VCC_DP_BUFF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 1, 1), + [F_VCC_DM_BUFF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 0, 0), + [F_VCC_EXTCLKENBL] = REG_FIELD(VCC_UCD_FCTRL_EN, 15, 15), + [F_VCC_PLSTESTEN] = REG_FIELD(VCC_UCD_FCTRL_EN, 14, 14), + [F_VCC_UCDSWEN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 10, 10), + [F_VCC_RREF_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 9, 9), + [F_VCC_DPPU_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 8, 8), + [F_VCC_DPREF_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 7, 7), + [F_VCC_DMREF_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 6, 6), + [F_VCC_DPDET_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 5, 5), + [F_VCC_DMDET_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 4, 4), + [F_VCC_DPSINK_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 3, 3), + [F_VCC_DMSINK_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 2, 2), + [F_VCC_DP_BUFF_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 1, 1), + [F_VCC_DM_BUFF_EN_TSTENB] = REG_FIELD(VCC_UCD_FCTRL_EN, 0, 0), + + [F_VBUS_BCSRETRY] = REG_FIELD(VBUS_UCD_SET, 12, 12), + [F_VBUS_ADCRTRY] = REG_FIELD(VBUS_UCD_SET, 8, 8), + [F_VBUS_USBDETEN] = REG_FIELD(VBUS_UCD_SET, 7, 7), + [F_VBUS_IDRDETEN] = REG_FIELD(VBUS_UCD_SET, 6, 6), + [F_VBUS_ENUMRDY] = REG_FIELD(VBUS_UCD_SET, 5, 5), + [F_VBUS_ADCPOLEN] = REG_FIELD(VBUS_UCD_SET, 4, 4), + [F_VBUS_DCDMODE] = REG_FIELD(VBUS_UCD_SET, 3, 3), + [F_VBUS_USB_SW_EN] = REG_FIELD(VBUS_UCD_SET, 1, 1), + [F_VBUS_USB_SW] = REG_FIELD(VBUS_UCD_SET, 0, 0), + [F_VBUS_DCDFAIL] = REG_FIELD(VBUS_UCD_STATUS, 15, 15), + [F_VBUS_CHGPORT] = REG_FIELD(VBUS_UCD_STATUS, 12, 13), + [F_VBUS_PUPDET] = REG_FIELD(VBUS_UCD_STATUS, 11, 11), + [F_VBUS_VBUS_VLD] = REG_FIELD(VBUS_UCD_STATUS, 7, 7), + [F_VBUS_CHGDET] = REG_FIELD(VBUS_UCD_STATUS, 6, 6), + [F_VBUS_OTGDET] = REG_FIELD(VBUS_UCD_STATUS, 3, 3), + [F_VBUS_VBINOP] = REG_FIELD(VBUS_IDD_STATUS, 6, 6), + [F_VBUS_EXTID] = REG_FIELD(VBUS_IDD_STATUS, 5, 5), + [F_VBUS_IDRDET] = REG_FIELD(VBUS_IDD_STATUS, 4, 4), + [F_VBUS_INDO] = REG_FIELD(VBUS_IDD_STATUS, 0, 3), + [F_VBUS_UCDSWEN] = REG_FIELD(VCC_UCD_FCTRL_SET, 10, 10), + [F_VBUS_RREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 9, 9), + [F_VBUS_DPPU_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 8, 8), + [F_VBUS_DPREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 7, 7), + [F_VBUS_DMREF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 6, 6), + [F_VBUS_DPDET_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 5, 5), + [F_VBUS_DMDET_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 4, 4), + [F_VBUS_DPSINK_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 3, 3), + [F_VBUS_DMSINK_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 2, 2), + [F_VBUS_DP_BUFF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 1, 1), + [F_VBUS_DM_BUFF_EN] = REG_FIELD(VCC_UCD_FCTRL_SET, 0, 0), + + [F_VBUS_EXTCLKENBL] = REG_FIELD(VBUS_UCD_FCTRL_EN, 15, 15), + [F_VBUS_PLSTESTEN] = REG_FIELD(VBUS_UCD_FCTRL_EN, 14, 14), + [F_VBUS_UCDSWEN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 10, 10), + [F_VBUS_RREF_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 9, 9), + [F_VBUS_DPPU_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 8, 8), + [F_VBUS_DPREF_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 7, 7), + [F_VBUS_DMREF_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 6, 6), + [F_VBUS_DPDET_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 5, 5), + [F_VBUS_DMDET_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 4, 4), + [F_VBUS_DPSINK_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 3, 3), + [F_VBUS_DMSINK_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 2, 2), + [F_VBUS_DP_BUFF_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 1, 1), + [F_VBUS_DM_BUFF_EN_TSTENB] = REG_FIELD(VBUS_UCD_FCTRL_EN, 0, 0), + + [F_CHIP_ID] = REG_FIELD(CHIP_ID, 0, 15), + [F_CHIP_REV] = REG_FIELD(CHIP_REV, 0, 15), + [F_ONE_CELL_MODE] = REG_FIELD(IC_SET1, 11, 11), + [F_cell] = REG_FIELD(IC_SET1, 1, 1), + [F_VACP_AUTO_DISCHG] = REG_FIELD(IC_SET1, 9, 9), + [F_VACP_LOAD] = REG_FIELD(IC_SET1, 8, 8), + [F_ACOK_POL] = REG_FIELD(IC_SET1, 1, 1), + [F_ACOK_DISEN] = REG_FIELD(IC_SET1, 0, 0), + [F_DEBUG_SET1] = REG_FIELD(IC_SET2, 4, 8), + [F_DEBUG_SET0] = REG_FIELD(IC_SET2, 0, 0), + [F_MONRST_STATE] = REG_FIELD(SYSTEM_STATUS, 6, 6), + [F_ALMRST_STATE] = REG_FIELD(SYSTEM_STATUS, 5, 5), + [F_CHGRST_STATE] = REG_FIELD(SYSTEM_STATUS, 4, 4), + [F_OTPLD_STATE] = REG_FIELD(SYSTEM_STATUS, 1, 1), + [F_ALLRST_STATE] = REG_FIELD(SYSTEM_STATUS, 0, 0), + [F_PROTECT_SET] = REG_FIELD(PROTECT_SET, 0, 15), + [F_MAP_SET] = REG_FIELD(MAP_SET, 0, 15), + [F_ADCINTERVAL] = REG_FIELD(VM_CTRL_SET, 14, 15), + [F_ADCMOD] = REG_FIELD(VM_CTRL_SET, 12, 13), + [F_ADCTMOD] = REG_FIELD(VM_CTRL_SET, 10, 11), + [F_EXTIADPEN] = REG_FIELD(VM_CTRL_SET, 9, 9), + [F_VSYSENB] = REG_FIELD(VM_CTRL_SET, 8, 8), + [F_VCCENB] = REG_FIELD(VM_CTRL_SET, 7, 7), + [F_VBUSENB] = REG_FIELD(VM_CTRL_SET, 6, 6), + [F_VACPENB] = REG_FIELD(VM_CTRL_SET, 5, 5), + [F_IACPENB] = REG_FIELD(VM_CTRL_SET, 4, 4), + [F_THERMENB] = REG_FIELD(VM_CTRL_SET, 3, 3), + [F_VBATENB] = REG_FIELD(VM_CTRL_SET, 2, 2), + [F_IBATMENB] = REG_FIELD(VM_CTRL_SET, 1, 1), + [F_IBATPENB] = REG_FIELD(VM_CTRL_SET, 0, 0), + [F_TMPTHR1B] = REG_FIELD(THERM_WINDOW_SET1, 8, 15), + [F_TMPTHR1A] = REG_FIELD(THERM_WINDOW_SET1, 0, 7), + [F_TMPTHR2B] = REG_FIELD(THERM_WINDOW_SET2, 8, 15), + [F_TMPTHR2A] = REG_FIELD(THERM_WINDOW_SET2, 0, 7), + [F_TMPTHR3B] = REG_FIELD(THERM_WINDOW_SET3, 8, 15), + [F_TMPTHR3A] = REG_FIELD(THERM_WINDOW_SET3, 0, 7), + [F_TMPTHR4B] = REG_FIELD(THERM_WINDOW_SET4, 8, 15), + [F_TMPTHR4A] = REG_FIELD(THERM_WINDOW_SET4, 0, 7), + [F_TMPTHR5B] = REG_FIELD(THERM_WINDOW_SET5, 8, 15), + [F_TMPTHR5A] = REG_FIELD(THERM_WINDOW_SET5, 0, 7), + [F_IBATP_TH_SET] = REG_FIELD(IBATP_TH_SET, 0, 14), + [F_IBATM_TH_SET] = REG_FIELD(IBATM_TH_SET, 0, 14), + [F_VBAT_TH_SET] = REG_FIELD(VBAT_TH_SET, 0, 14), + [F_THERM_TH_SET] = REG_FIELD(THERM_TH_SET, 0, 7), + [F_IACP_TH_SET] = REG_FIELD(IACP_TH_SET, 0, 14), + [F_VACP_TH_SET] = REG_FIELD(VACP_TH_SET, 0, 14), + [F_VBUS_TH_SET] = REG_FIELD(VBUS_TH_SET, 0, 14), + [F_VCC_TH_SET] = REG_FIELD(VCC_TH_SET, 0, 14), + [F_VSYS_TH_SET] = REG_FIELD(VSYS_TH_SET, 0, 14), + [F_EXTIADP_TH_SET] = REG_FIELD(EXTIADP_TH_SET, 0, 11), + [F_IBATP_VAL] = REG_FIELD(IBATP_VAL, 0, 14), + [F_IBATP_AVE_VAL] = REG_FIELD(IBATP_AVE_VAL, 0, 14), + [F_IBATM_VAL] = REG_FIELD(IBATM_VAL, 0, 14), + [F_IBATM_AVE_VAL] = REG_FIELD(IBATM_AVE_VAL, 0, 14), + [F_VBAT_VAL] = REG_FIELD(VBAT_VAL, 0, 14), + [F_VBAT_AVE_VAL] = REG_FIELD(VBAT_AVE_VAL, 0, 14), + [F_THERM_VAL] = REG_FIELD(THERM_VAL, 0, 7), + [F_VTH_VAL] = REG_FIELD(VTH_VAL, 0, 11), + [F_IACP_VAL] = REG_FIELD(IACP_VAL, 0, 14), + [F_IACP_AVE_VAL] = REG_FIELD(IACP_AVE_VAL, 0, 14), + [F_VACP_VAL] = REG_FIELD(VACP_VAL, 0, 14), + [F_VACP_AVE_VAL] = REG_FIELD(VACP_AVE_VAL, 0, 14), + [F_VBUS_VAL] = REG_FIELD(VBUS_VAL, 0, 14), + [F_VBUS_AVE_VAL] = REG_FIELD(VBUS_AVE_VAL, 0, 14), + [F_VCC_VAL] = REG_FIELD(VCC_VAL, 0, 14), + [F_VCC_AVE_VAL] = REG_FIELD(VCC_AVE_VAL, 0, 14), + [F_VSYS_VAL] = REG_FIELD(VSYS_VAL, 0, 14), + [F_VSYS_AVE_VAL] = REG_FIELD(VSYS_AVE_VAL, 0, 14), + [F_EXTIADP_VAL] = REG_FIELD(EXTIADP_VAL, 0, 11), + [F_EXTIADP_AVE_VAL] = REG_FIELD(EXTIADP_AVE_VAL, 0, 11), + [F_VACPCLPS_TH_SET] = REG_FIELD(VACPCLPS_TH_SET, 7, 14), + [F_INT7_SET] = REG_FIELD(INT7_SET, 0, 15), + [F_INT6_SET] = REG_FIELD(INT6_SET, 0, 13), + [F_INT5_SET] = REG_FIELD(INT5_SET, 0, 13), + [F_INT4_SET] = REG_FIELD(INT4_SET, 0, 9), + [F_INT3_SET] = REG_FIELD(INT3_SET, 0, 15), + [F_INT2_SET] = REG_FIELD(INT2_SET, 0, 15), + [F_INT1_SET] = REG_FIELD(INT1_SET, 0, 15), + [F_INT0_SET] = REG_FIELD(INT0_SET, 0, 7), + [F_VBUS_RBUV_DET] = REG_FIELD(INT1_SET, 15, 15), + [F_VBUS_RBUV_RES] = REG_FIELD(INT1_SET, 14, 14), + [F_VBUS_TH_DET] = REG_FIELD(INT1_SET, 9, 9), + [F_VBUS_TH_RES] = REG_FIELD(INT1_SET, 8, 8), + [F_VBUS_IIN_MOD] = REG_FIELD(INT1_SET, 6, 6), + [F_VBUS_OV_DET] = REG_FIELD(INT1_SET, 5, 5), + [F_VBUS_OV_RES] = REG_FIELD(INT1_SET, 4, 4), + [F_VBUS_CLPS_DET] = REG_FIELD(INT1_SET, 3, 3), + [F_VBUS_CLPS] = REG_FIELD(INT1_SET, 2, 2), + [F_VBUS_DET] = REG_FIELD(INT1_SET, 1, 1), + [F_VBUS_RES] = REG_FIELD(INT1_SET, 0, 0), + [F_VCC_RBUV_DET] = REG_FIELD(INT2_SET, 15, 15), + [F_VCC_RBUV_RES] = REG_FIELD(INT2_SET, 14, 14), + [F_VCC_TH_DET] = REG_FIELD(INT2_SET, 9, 9), + [F_VCC_TH_RES] = REG_FIELD(INT2_SET, 8, 8), + [F_VCC_IIN_MOD] = REG_FIELD(INT2_SET, 6, 6), + [F_VCC_OVP_DET] = REG_FIELD(INT2_SET, 5, 5), + [F_VCC_OVP_RES] = REG_FIELD(INT2_SET, 4, 4), + [F_VCC_CLPS_DET] = REG_FIELD(INT2_SET, 3, 3), + [F_VCC_CLPS_RES] = REG_FIELD(INT2_SET, 2, 2), + [F_VCC_DET] = REG_FIELD(INT2_SET, 1, 1), + [F_VCC_RES] = REG_FIELD(INT2_SET, 0, 0), + [F_TH_DET] = REG_FIELD(INT3_SET, 15, 15), + [F_TH_RMV] = REG_FIELD(INT3_SET, 14, 14), + [F_TMP_OUT_DET] = REG_FIELD(INT3_SET, 11, 11), + [F_TMP_OUT_RES] = REG_FIELD(INT3_SET, 10, 10), + [F_VBAT_TH_DET] = REG_FIELD(INT3_SET, 9, 9), + [F_VBAT_TH_RES] = REG_FIELD(INT3_SET, 8, 8), + [F_IBAT_SHORT_DET] = REG_FIELD(INT3_SET, 7, 7), + [F_IBAT_SHORT_RES] = REG_FIELD(INT3_SET, 6, 6), + [F_VBAT_OV_DET] = REG_FIELD(INT3_SET, 5, 5), + [F_VBAT_OV_RES] = REG_FIELD(INT3_SET, 4, 4), + [F_BAT_ASSIST_DET] = REG_FIELD(INT3_SET, 3, 3), + [F_BAT_ASSIST_RES] = REG_FIELD(INT3_SET, 2, 2), + [F_VSYS_TH_DET] = REG_FIELD(INT4_SET, 9, 9), + [F_VSYS_TH_RES] = REG_FIELD(INT4_SET, 8, 8), + [F_VSYS_OV_DET] = REG_FIELD(INT4_SET, 5, 5), + [F_VSYS_OV_RES] = REG_FIELD(INT4_SET, 4, 4), + [F_VSYS_SHT_DET] = REG_FIELD(INT4_SET, 3, 3), + [F_VSYS_SHT_RES] = REG_FIELD(INT4_SET, 2, 2), + [F_VSYS_UV_DET] = REG_FIELD(INT4_SET, 1, 1), + [F_VSYS_UV_RES] = REG_FIELD(INT4_SET, 0, 0), + [F_OTP_LOAD_DONE] = REG_FIELD(INT5_SET, 13, 13), + [F_PWR_ON] = REG_FIELD(INT5_SET, 12, 12), + [F_EXTIADP_TRNS] = REG_FIELD(INT5_SET, 11, 11), + [F_EXTIADP_TH_DET] = REG_FIELD(INT5_SET, 9, 9), + [F_EXIADP_TH_RES] = REG_FIELD(INT5_SET, 8, 8), + [F_BAT_MNT_DET] = REG_FIELD(INT5_SET, 7, 7), + [F_BAT_MNT_RES] = REG_FIELD(INT5_SET, 6, 6), + [F_TSD_DET] = REG_FIELD(INT5_SET, 5, 5), + [F_TSD_RES] = REG_FIELD(INT5_SET, 4, 4), + [F_CHGWDT_EXP] = REG_FIELD(INT5_SET, 3, 3), + [F_THERMWDT_EXP] = REG_FIELD(INT5_SET, 2, 2), + [F_TMP_TRNS] = REG_FIELD(INT5_SET, 1, 1), + [F_CHG_TRNS] = REG_FIELD(INT5_SET, 0, 0), + [F_VBUS_UCD_PORT_DET] = REG_FIELD(INT6_SET, 13, 13), + [F_VBUS_UCD_UCHG_DET] = REG_FIELD(INT6_SET, 12, 12), + [F_VBUS_UCD_URID_RMV] = REG_FIELD(INT6_SET, 11, 11), + [F_VBUS_UCD_OTG_DET] = REG_FIELD(INT6_SET, 10, 10), + [F_VBUS_UCD_URID_MOD] = REG_FIELD(INT6_SET, 8, 8), + [F_VCC_UCD_PORT_DET] = REG_FIELD(INT6_SET, 5, 5), + [F_VCC_UCD_UCHG_DET] = REG_FIELD(INT6_SET, 4, 4), + [F_VCC_UCD_URID_RMV] = REG_FIELD(INT6_SET, 3, 3), + [F_VCC_UCD_OTG_DET] = REG_FIELD(INT6_SET, 2, 2), + [F_VCC_UCD_URID_MOD] = REG_FIELD(INT6_SET, 0, 0), + [F_PROCHOT_DET] = REG_FIELD(INT7_SET, 15, 15), + [F_PROCHOT_RES] = REG_FIELD(INT7_SET, 14, 14), + [F_VACP_DET] = REG_FIELD(INT7_SET, 11, 11), + [F_VACP_RES] = REG_FIELD(INT7_SET, 10, 10), + [F_VACP_TH_DET] = REG_FIELD(INT7_SET, 9, 9), + [F_VACP_TH_RES] = REG_FIELD(INT7_SET, 8, 8), + [F_IACP_TH_DET] = REG_FIELD(INT7_SET, 7, 7), + [F_IACP_THE_RES] = REG_FIELD(INT7_SET, 6, 6), + [F_THERM_TH_DET] = REG_FIELD(INT7_SET, 5, 5), + [F_THERM_TH_RES] = REG_FIELD(INT7_SET, 4, 4), + [F_IBATM_TH_DET] = REG_FIELD(INT7_SET, 3, 3), + [F_IBATM_TH_RES] = REG_FIELD(INT7_SET, 2, 2), + [F_IBATP_TH_DET] = REG_FIELD(INT7_SET, 1, 1), + [F_IBATP_TH_RES] = REG_FIELD(INT7_SET, 0, 0), + [F_INT7_STATUS] = REG_FIELD(INT7_STATUS, 0, 15), + [F_INT6_STATUS] = REG_FIELD(INT6_STATUS, 0, 13), + [F_INT5_STATUS] = REG_FIELD(INT5_STATUS, 0, 13), + [F_INT4_STATUS] = REG_FIELD(INT4_STATUS, 0, 9), + [F_INT3_STATUS] = REG_FIELD(INT3_STATUS, 0, 15), + [F_INT2_STATUS] = REG_FIELD(INT2_STATUS, 0, 15), + [F_INT1_STATUS] = REG_FIELD(INT1_STATUS, 0, 15), + [F_INT0_STATUS] = REG_FIELD(INT0_STATUS, 0, 7), + [F_ILIM_DECREASE] = REG_FIELD(OTPREG0, 0, 15), + [F_RESERVE_OTPREG1] = REG_FIELD(OTPREG1, 0, 15), + [F_POWER_SAVE_MODE] = REG_FIELD(SMBREG, 0, 15), + [F_DEBUG_MODE_SET] = REG_FIELD(DEBUG_MODE_SET, 0, 15), + [F_DEBUG0x14] = REG_FIELD(DEBUG0x14, 0, 15), + [F_DEBUG0x1A] = REG_FIELD(DEBUG0x1A, 0, 15), +}; + +/* CHGSTM_STATEs */ +#define CHGSTM_SUSPEND 0x00 +#define CHGSTM_TRICKLE_CHARGE 0x01 +#define CHGSTM_PRE_CHARGE 0x02 +#define CHGSTM_FAST_CHARGE 0x03 +#define CHGSTM_TOP_OFF 0x04 +#define CHGSTM_DONE 0x05 +#define CHGSTM_OTG 0x08 +#define CHGSTM_OTG_DONE 0x09 +#define CHGSTM_TEMPERATURE_ERROR_1 0x10 +#define CHGSTM_TEMPERATURE_ERROR_2 0x11 +#define CHGSTM_TEMPERATURE_ERROR_3 0x12 +#define CHGSTM_TEMPERATURE_ERROR_4 0x13 +#define CHGSTM_TEMPERATURE_ERROR_5 0x14 +#define CHGSTM_TEMPERATURE_ERROR_6 0x15 +#define CHGSTM_TEMPERATURE_ERROR_7 0x18 +#define CHGSTM_THERMAL_SHUT_DOWN_1 0x20 +#define CHGSTM_THERMAL_SHUT_DOWN_2 0x21 +#define CHGSTM_THERMAL_SHUT_DOWN_3 0x22 +#define CHGSTM_THERMAL_SHUT_DOWN_4 0x23 +#define CHGSTM_THERMAL_SHUT_DOWN_5 0x24 +#define CHGSTM_THERMAL_SHUT_DOWN_6 0x25 +#define CHGSTM_THERMAL_SHUT_DOWN_7 0x28 +#define CHGSTM_BATTERY_ERROR 0x40 + +/* VBAT_VSYS_STATUS */ +#define STATUS_VSYS_OV BIT(15) +#define STATUS_VSYS_SSD BIT(14) +#define STATUS_VSYS_SCP BIT(13) +#define STATUS_VSYS_UVN BIT(12) +#define STATUS_IBAT_SHORT BIT(6) +#define STATUS_VBAT_OV BIT(3) +#define STATUS_DEAD_BAT BIT(0) + +/* VBUS_VCC_STATUS */ +#define STATUS_VACP_DET BIT(12) +#define STATUS_VCC_OVP BIT(11) +#define STATUS_ILIM_VCC_MOD BIT(10) +#define STATUS_VCC_CLPS BIT(9) +#define STATUS_VCC_DET BIT(8) +#define STATUS_VBUS_OVP BIT(3) +#define STATUS_ILIM_VBUS_MOD BIT(2) +#define STATUS_VBUS_CLPS BIT(1) +#define STATUS_VBUS_DET BIT(0) + +/* Interrupt set/status definitions */ + +/* INT 0 */ +#define INT0_INT7_STATUS BIT(7) +#define INT0_INT6_STATUS BIT(6) +#define INT0_INT5_STATUS BIT(5) +#define INT0_INT4_STATUS BIT(4) +#define INT0_INT3_STATUS BIT(3) +#define INT0_INT2_STATUS BIT(2) +#define INT0_INT1_STATUS BIT(1) +#define INT0_INT0_STATUS BIT(0) +#define INT0_ALL 0xff + +/* INT 1 */ +#define VBUS_RBUV_DET BIT(15) +#define VBUS_RBUV_RES BIT(14) +#define VBUS_TH_DET BIT(9) +#define VBUS_TH_RES BIT(8) +#define VBUS_IIN_MOD BIT(6) +#define VBUS_OV_DET BIT(5) +#define VBUS_OV_RES BIT(4) +#define VBUS_CLPS_DET BIT(3) +#define VBUS_CLPS BIT(2) +#define VBUS_DET BIT(1) +#define VBUS_RES BIT(0) +#define INT1_ALL (VBUS_RBUV_DET|\ + VBUS_RBUV_RES|\ + VBUS_TH_DET |\ + VBUS_TH_RES |\ + VBUS_IIN_MOD|\ + VBUS_OV_DET |\ + VBUS_OV_RES |\ + VBUS_CLPS_DET |\ + VBUS_CLPS |\ + VBUS_DET |\ + VBUS_RES) + +/* INT 2 */ +#define VCC_RBUV_DET BIT(15) +#define VCC_RBUV_RES BIT(14) +#define VCC_TH_DET BIT(9) +#define VCC_TH_RES BIT(8) +#define VCC_IIN_MOD BIT(6) +#define VCC_OVP_DET BIT(5) +#define VCC_OVP_RES BIT(4) +#define VCC_CLPS_DET BIT(3) +#define VCC_CLPS_RES BIT(2) +#define VCC_DET BIT(1) +#define VCC_RES BIT(0) +#define INT2_ALL (VCC_RBUV_DET |\ + VCC_RBUV_RES |\ + VCC_TH_DET |\ + VCC_TH_RES |\ + VCC_IIN_MOD |\ + VCC_OVP_DET |\ + VCC_OVP_RES |\ + VCC_CLPS_DET |\ + VCC_CLPS_RES |\ + VCC_DET |\ + VCC_RES) +/* INT 3 */ +#define TH_DET BIT(15) +#define TH_RMV BIT(14) +#define TMP_OUT_DET BIT(11) +#define TMP_OUT_RES BIT(10) +#define VBAT_TH_DET BIT(9) +#define VBAT_TH_RES BIT(8) +#define IBAT_SHORT_DET BIT(7) +#define IBAT_SHORT_RES BIT(6) +#define VBAT_OV_DET BIT(5) +#define VBAT_OV_RES BIT(4) +#define BAT_ASSIST_DET BIT(3) +#define BAT_ASSIST_RES BIT(2) +#define INT3_ALL (TH_DET |\ + TH_RMV |\ + TMP_OUT_DET |\ + TMP_OUT_RES |\ + VBAT_TH_DET |\ + VBAT_TH_RES |\ + IBAT_SHORT_DET |\ + IBAT_SHORT_RES |\ + VBAT_OV_DET |\ + VBAT_OV_RES |\ + BAT_ASSIST_DET |\ + BAT_ASSIST_RES) + +/* INT 4 */ +#define VSYS_TH_DET BIT(9) +#define VSYS_TH_RES BIT(8) +#define VSYS_OV_DET BIT(5) +#define VSYS_OV_RES BIT(4) +#define VSYS_SHT_DET BIT(3) +#define VSYS_SHT_RES BIT(2) +#define VSYS_UV_DET BIT(1) +#define VSYS_UV_RES BIT(0) +#define INT4_ALL (VSYS_TH_DET |\ + VSYS_TH_RES |\ + VSYS_OV_DET |\ + VSYS_OV_RES |\ + VSYS_SHT_DET |\ + VSYS_SHT_RES |\ + VSYS_UV_DET |\ + VSYS_UV_RES) + +/* INT 5*/ +#define OTP_LOAD_DONE BIT(13) +#define PWR_ON BIT(12) +#define EXTIADP_TRNS BIT(11) +#define EXTIADP_TH_DET BIT(9) +#define EXIADP_TH_RES BIT(8) +#define BAT_MNT_DET BIT(7) +#define BAT_MNT_RES BIT(6) +#define TSD_DET BIT(5) +#define TSD_RES BIT(4) +#define CHGWDT_EXP BIT(3) +#define THERMWDT_EXP BIT(2) +#define TMP_TRNS BIT(1) +#define CHG_TRNS BIT(0) +#define INT5_ALL (OTP_LOAD_DONE |\ + PWR_ON |\ + EXTIADP_TRNS |\ + EXTIADP_TH_DET |\ + EXIADP_TH_RES |\ + BAT_MNT_DET |\ + BAT_MNT_RES |\ + TSD_DET |\ + TSD_RES |\ + CHGWDT_EXP |\ + THERMWDT_EXP |\ + TMP_TRNS |\ + CHG_TRNS) + +/* INT 6*/ +#define VBUS_UCD_PORT_DET BIT(13) +#define VBUS_UCD_UCHG_DET BIT(12) +#define VBUS_UCD_URID_RMV BIT(11) +#define VBUS_UCD_OTG_DET BIT(10) +#define VBUS_UCD_URID_MOD BIT(8) +#define VCC_UCD_PORT_DET BIT(5) +#define VCC_UCD_UCHG_DET BIT(4) +#define VCC_UCD_URID_RMV BIT(3) +#define VCC_UCD_OTG_DET BIT(2) +#define VCC_UCD_URID_MOD BIT(0) +#define INT6_ALL (VBUS_UCD_PORT_DET |\ + VBUS_UCD_UCHG_DET |\ + VBUS_UCD_URID_RMV |\ + VBUS_UCD_OTG_DET |\ + VBUS_UCD_URID_MOD |\ + VCC_UCD_PORT_DET |\ + VCC_UCD_UCHG_DET |\ + VCC_UCD_URID_RMV |\ + VCC_UCD_OTG_DET |\ + VCC_UCD_URID_MOD) + +/* INT 7 */ +#define PROCHOT_DET BIT(15) +#define PROCHOT_RES BIT(14) +#define VACP_DET BIT(11) +#define VACP_RES BIT(10) +#define VACP_TH_DET BIT(9) +#define VACP_TH_RES BIT(8) +#define IACP_TH_DET BIT(7) +#define IACP_THE_RES BIT(6) +#define THERM_TH_DET BIT(5) +#define THERM_TH_RES BIT(4) +#define IBATM_TH_DET BIT(3) +#define IBATM_TH_RES BIT(2) +#define IBATP_TH_DET BIT(1) +#define IBATP_TH_RES BIT(0) +#define INT7_ALL (PROCHOT_DET |\ + PROCHOT_RES |\ + VACP_DET |\ + VACP_RES |\ + VACP_TH_DET |\ + VACP_TH_RES |\ + IACP_TH_DET |\ + IACP_THE_RES |\ + THERM_TH_DET |\ + THERM_TH_RES |\ + IBATM_TH_DET |\ + IBATM_TH_RES |\ + IBATP_TH_DET |\ + IBATP_TH_RES) + +/* SYSTEM_CTRL_SET*/ +#define MONRST BIT(6) +#define ALMRST BIT(5) +#define CHGRST BIT(4) +#define OTPLD BIT(1) +#define ALLRST BIT(0) + +/* F_BATTEMP */ +#define ROOM 0x0 +#define HOT1 0x1 +#define HOT2 0x2 +#define HOT3 0x3 +#define COLD1 0x4 +#define COLD2 0x5 +#define TEMP_DIS 0x6 +#define BATT_OPEN 0x7 + +#endif diff --git a/drivers/power/supply/bq2415x_charger.c b/drivers/power/supply/bq2415x_charger.c new file mode 100644 index 000000000..5724001e6 --- /dev/null +++ b/drivers/power/supply/bq2415x_charger.c @@ -0,0 +1,1793 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * bq2415x charger driver + * + * Copyright (C) 2011-2013 Pali Rohár <pali@kernel.org> + * + * Datasheets: + * https://www.ti.com/product/bq24150 + * https://www.ti.com/product/bq24150a + * https://www.ti.com/product/bq24152 + * https://www.ti.com/product/bq24153 + * https://www.ti.com/product/bq24153a + * https://www.ti.com/product/bq24155 + * https://www.ti.com/product/bq24157s + * https://www.ti.com/product/bq24158 + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/param.h> +#include <linux/err.h> +#include <linux/workqueue.h> +#include <linux/sysfs.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/idr.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/acpi.h> + +#include <linux/power/bq2415x_charger.h> + +/* timeout for resetting chip timer */ +#define BQ2415X_TIMER_TIMEOUT 10 + +#define BQ2415X_REG_STATUS 0x00 +#define BQ2415X_REG_CONTROL 0x01 +#define BQ2415X_REG_VOLTAGE 0x02 +#define BQ2415X_REG_VENDER 0x03 +#define BQ2415X_REG_CURRENT 0x04 + +/* reset state for all registers */ +#define BQ2415X_RESET_STATUS BIT(6) +#define BQ2415X_RESET_CONTROL (BIT(4)|BIT(5)) +#define BQ2415X_RESET_VOLTAGE (BIT(1)|BIT(3)) +#define BQ2415X_RESET_CURRENT (BIT(0)|BIT(3)|BIT(7)) + +/* status register */ +#define BQ2415X_BIT_TMR_RST 7 +#define BQ2415X_BIT_OTG 7 +#define BQ2415X_BIT_EN_STAT 6 +#define BQ2415X_MASK_STAT (BIT(4)|BIT(5)) +#define BQ2415X_SHIFT_STAT 4 +#define BQ2415X_BIT_BOOST 3 +#define BQ2415X_MASK_FAULT (BIT(0)|BIT(1)|BIT(2)) +#define BQ2415X_SHIFT_FAULT 0 + +/* control register */ +#define BQ2415X_MASK_LIMIT (BIT(6)|BIT(7)) +#define BQ2415X_SHIFT_LIMIT 6 +#define BQ2415X_MASK_VLOWV (BIT(4)|BIT(5)) +#define BQ2415X_SHIFT_VLOWV 4 +#define BQ2415X_BIT_TE 3 +#define BQ2415X_BIT_CE 2 +#define BQ2415X_BIT_HZ_MODE 1 +#define BQ2415X_BIT_OPA_MODE 0 + +/* voltage register */ +#define BQ2415X_MASK_VO (BIT(2)|BIT(3)|BIT(4)|BIT(5)|BIT(6)|BIT(7)) +#define BQ2415X_SHIFT_VO 2 +#define BQ2415X_BIT_OTG_PL 1 +#define BQ2415X_BIT_OTG_EN 0 + +/* vender register */ +#define BQ2415X_MASK_VENDER (BIT(5)|BIT(6)|BIT(7)) +#define BQ2415X_SHIFT_VENDER 5 +#define BQ2415X_MASK_PN (BIT(3)|BIT(4)) +#define BQ2415X_SHIFT_PN 3 +#define BQ2415X_MASK_REVISION (BIT(0)|BIT(1)|BIT(2)) +#define BQ2415X_SHIFT_REVISION 0 + +/* current register */ +#define BQ2415X_MASK_RESET BIT(7) +#define BQ2415X_MASK_VI_CHRG (BIT(4)|BIT(5)|BIT(6)) +#define BQ2415X_SHIFT_VI_CHRG 4 +/* N/A BIT(3) */ +#define BQ2415X_MASK_VI_TERM (BIT(0)|BIT(1)|BIT(2)) +#define BQ2415X_SHIFT_VI_TERM 0 + + +enum bq2415x_command { + BQ2415X_TIMER_RESET, + BQ2415X_OTG_STATUS, + BQ2415X_STAT_PIN_STATUS, + BQ2415X_STAT_PIN_ENABLE, + BQ2415X_STAT_PIN_DISABLE, + BQ2415X_CHARGE_STATUS, + BQ2415X_BOOST_STATUS, + BQ2415X_FAULT_STATUS, + + BQ2415X_CHARGE_TERMINATION_STATUS, + BQ2415X_CHARGE_TERMINATION_ENABLE, + BQ2415X_CHARGE_TERMINATION_DISABLE, + BQ2415X_CHARGER_STATUS, + BQ2415X_CHARGER_ENABLE, + BQ2415X_CHARGER_DISABLE, + BQ2415X_HIGH_IMPEDANCE_STATUS, + BQ2415X_HIGH_IMPEDANCE_ENABLE, + BQ2415X_HIGH_IMPEDANCE_DISABLE, + BQ2415X_BOOST_MODE_STATUS, + BQ2415X_BOOST_MODE_ENABLE, + BQ2415X_BOOST_MODE_DISABLE, + + BQ2415X_OTG_LEVEL, + BQ2415X_OTG_ACTIVATE_HIGH, + BQ2415X_OTG_ACTIVATE_LOW, + BQ2415X_OTG_PIN_STATUS, + BQ2415X_OTG_PIN_ENABLE, + BQ2415X_OTG_PIN_DISABLE, + + BQ2415X_VENDER_CODE, + BQ2415X_PART_NUMBER, + BQ2415X_REVISION, +}; + +enum bq2415x_chip { + BQUNKNOWN, + BQ24150, + BQ24150A, + BQ24151, + BQ24151A, + BQ24152, + BQ24153, + BQ24153A, + BQ24155, + BQ24156, + BQ24156A, + BQ24157S, + BQ24158, +}; + +static char *bq2415x_chip_name[] = { + "unknown", + "bq24150", + "bq24150a", + "bq24151", + "bq24151a", + "bq24152", + "bq24153", + "bq24153a", + "bq24155", + "bq24156", + "bq24156a", + "bq24157s", + "bq24158", +}; + +struct bq2415x_device { + struct device *dev; + struct bq2415x_platform_data init_data; + struct power_supply *charger; + struct power_supply_desc charger_desc; + struct delayed_work work; + struct device_node *notify_node; + struct notifier_block nb; + enum bq2415x_mode reported_mode;/* mode reported by hook function */ + enum bq2415x_mode mode; /* currently configured mode */ + enum bq2415x_chip chip; + const char *timer_error; + char *model; + char *name; + int autotimer; /* 1 - if driver automatically reset timer, 0 - not */ + int automode; /* 1 - enabled, 0 - disabled; -1 - not supported */ + int id; +}; + +/* each registered chip must have unique id */ +static DEFINE_IDR(bq2415x_id); + +static DEFINE_MUTEX(bq2415x_id_mutex); +static DEFINE_MUTEX(bq2415x_timer_mutex); +static DEFINE_MUTEX(bq2415x_i2c_mutex); + +/**** i2c read functions ****/ + +/* read value from register */ +static int bq2415x_i2c_read(struct bq2415x_device *bq, u8 reg) +{ + struct i2c_client *client = to_i2c_client(bq->dev); + struct i2c_msg msg[2]; + u8 val; + int ret; + + if (!client->adapter) + return -ENODEV; + + msg[0].addr = client->addr; + msg[0].flags = 0; + msg[0].buf = ® + msg[0].len = sizeof(reg); + msg[1].addr = client->addr; + msg[1].flags = I2C_M_RD; + msg[1].buf = &val; + msg[1].len = sizeof(val); + + mutex_lock(&bq2415x_i2c_mutex); + ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)); + mutex_unlock(&bq2415x_i2c_mutex); + + if (ret < 0) + return ret; + + return val; +} + +/* read value from register, apply mask and right shift it */ +static int bq2415x_i2c_read_mask(struct bq2415x_device *bq, u8 reg, + u8 mask, u8 shift) +{ + int ret; + + if (shift > 8) + return -EINVAL; + + ret = bq2415x_i2c_read(bq, reg); + if (ret < 0) + return ret; + return (ret & mask) >> shift; +} + +/* read value from register and return one specified bit */ +static int bq2415x_i2c_read_bit(struct bq2415x_device *bq, u8 reg, u8 bit) +{ + if (bit > 8) + return -EINVAL; + return bq2415x_i2c_read_mask(bq, reg, BIT(bit), bit); +} + +/**** i2c write functions ****/ + +/* write value to register */ +static int bq2415x_i2c_write(struct bq2415x_device *bq, u8 reg, u8 val) +{ + struct i2c_client *client = to_i2c_client(bq->dev); + struct i2c_msg msg[1]; + u8 data[2]; + int ret; + + data[0] = reg; + data[1] = val; + + msg[0].addr = client->addr; + msg[0].flags = 0; + msg[0].buf = data; + msg[0].len = ARRAY_SIZE(data); + + mutex_lock(&bq2415x_i2c_mutex); + ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)); + mutex_unlock(&bq2415x_i2c_mutex); + + /* i2c_transfer returns number of messages transferred */ + if (ret < 0) + return ret; + else if (ret != 1) + return -EIO; + + return 0; +} + +/* read value from register, change it with mask left shifted and write back */ +static int bq2415x_i2c_write_mask(struct bq2415x_device *bq, u8 reg, u8 val, + u8 mask, u8 shift) +{ + int ret; + + if (shift > 8) + return -EINVAL; + + ret = bq2415x_i2c_read(bq, reg); + if (ret < 0) + return ret; + + ret &= ~mask; + ret |= val << shift; + + return bq2415x_i2c_write(bq, reg, ret); +} + +/* change only one bit in register */ +static int bq2415x_i2c_write_bit(struct bq2415x_device *bq, u8 reg, + bool val, u8 bit) +{ + if (bit > 8) + return -EINVAL; + return bq2415x_i2c_write_mask(bq, reg, val, BIT(bit), bit); +} + +/**** global functions ****/ + +/* exec command function */ +static int bq2415x_exec_command(struct bq2415x_device *bq, + enum bq2415x_command command) +{ + int ret; + + switch (command) { + case BQ2415X_TIMER_RESET: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_STATUS, + 1, BQ2415X_BIT_TMR_RST); + case BQ2415X_OTG_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_STATUS, + BQ2415X_BIT_OTG); + case BQ2415X_STAT_PIN_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_STATUS, + BQ2415X_BIT_EN_STAT); + case BQ2415X_STAT_PIN_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_STATUS, 1, + BQ2415X_BIT_EN_STAT); + case BQ2415X_STAT_PIN_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_STATUS, 0, + BQ2415X_BIT_EN_STAT); + case BQ2415X_CHARGE_STATUS: + return bq2415x_i2c_read_mask(bq, BQ2415X_REG_STATUS, + BQ2415X_MASK_STAT, BQ2415X_SHIFT_STAT); + case BQ2415X_BOOST_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_STATUS, + BQ2415X_BIT_BOOST); + case BQ2415X_FAULT_STATUS: + return bq2415x_i2c_read_mask(bq, BQ2415X_REG_STATUS, + BQ2415X_MASK_FAULT, BQ2415X_SHIFT_FAULT); + + case BQ2415X_CHARGE_TERMINATION_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_CONTROL, + BQ2415X_BIT_TE); + case BQ2415X_CHARGE_TERMINATION_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 1, BQ2415X_BIT_TE); + case BQ2415X_CHARGE_TERMINATION_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 0, BQ2415X_BIT_TE); + case BQ2415X_CHARGER_STATUS: + ret = bq2415x_i2c_read_bit(bq, BQ2415X_REG_CONTROL, + BQ2415X_BIT_CE); + if (ret < 0) + return ret; + return ret > 0 ? 0 : 1; + case BQ2415X_CHARGER_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 0, BQ2415X_BIT_CE); + case BQ2415X_CHARGER_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 1, BQ2415X_BIT_CE); + case BQ2415X_HIGH_IMPEDANCE_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_CONTROL, + BQ2415X_BIT_HZ_MODE); + case BQ2415X_HIGH_IMPEDANCE_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 1, BQ2415X_BIT_HZ_MODE); + case BQ2415X_HIGH_IMPEDANCE_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 0, BQ2415X_BIT_HZ_MODE); + case BQ2415X_BOOST_MODE_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_CONTROL, + BQ2415X_BIT_OPA_MODE); + case BQ2415X_BOOST_MODE_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 1, BQ2415X_BIT_OPA_MODE); + case BQ2415X_BOOST_MODE_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_CONTROL, + 0, BQ2415X_BIT_OPA_MODE); + + case BQ2415X_OTG_LEVEL: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_VOLTAGE, + BQ2415X_BIT_OTG_PL); + case BQ2415X_OTG_ACTIVATE_HIGH: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_VOLTAGE, + 1, BQ2415X_BIT_OTG_PL); + case BQ2415X_OTG_ACTIVATE_LOW: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_VOLTAGE, + 0, BQ2415X_BIT_OTG_PL); + case BQ2415X_OTG_PIN_STATUS: + return bq2415x_i2c_read_bit(bq, BQ2415X_REG_VOLTAGE, + BQ2415X_BIT_OTG_EN); + case BQ2415X_OTG_PIN_ENABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_VOLTAGE, + 1, BQ2415X_BIT_OTG_EN); + case BQ2415X_OTG_PIN_DISABLE: + return bq2415x_i2c_write_bit(bq, BQ2415X_REG_VOLTAGE, + 0, BQ2415X_BIT_OTG_EN); + + case BQ2415X_VENDER_CODE: + return bq2415x_i2c_read_mask(bq, BQ2415X_REG_VENDER, + BQ2415X_MASK_VENDER, BQ2415X_SHIFT_VENDER); + case BQ2415X_PART_NUMBER: + return bq2415x_i2c_read_mask(bq, BQ2415X_REG_VENDER, + BQ2415X_MASK_PN, BQ2415X_SHIFT_PN); + case BQ2415X_REVISION: + return bq2415x_i2c_read_mask(bq, BQ2415X_REG_VENDER, + BQ2415X_MASK_REVISION, BQ2415X_SHIFT_REVISION); + } + return -EINVAL; +} + +/* detect chip type */ +static enum bq2415x_chip bq2415x_detect_chip(struct bq2415x_device *bq) +{ + struct i2c_client *client = to_i2c_client(bq->dev); + int ret = bq2415x_exec_command(bq, BQ2415X_PART_NUMBER); + + if (ret < 0) + return ret; + + switch (client->addr) { + case 0x6b: + switch (ret) { + case 0: + if (bq->chip == BQ24151A) + return bq->chip; + return BQ24151; + case 1: + if (bq->chip == BQ24150A || + bq->chip == BQ24152 || + bq->chip == BQ24155) + return bq->chip; + return BQ24150; + case 2: + if (bq->chip == BQ24153A) + return bq->chip; + return BQ24153; + default: + return BQUNKNOWN; + } + break; + + case 0x6a: + switch (ret) { + case 0: + if (bq->chip == BQ24156A) + return bq->chip; + return BQ24156; + case 2: + if (bq->chip == BQ24157S) + return bq->chip; + return BQ24158; + default: + return BQUNKNOWN; + } + break; + } + + return BQUNKNOWN; +} + +/* detect chip revision */ +static int bq2415x_detect_revision(struct bq2415x_device *bq) +{ + int ret = bq2415x_exec_command(bq, BQ2415X_REVISION); + int chip = bq2415x_detect_chip(bq); + + if (ret < 0 || chip < 0) + return -1; + + switch (chip) { + case BQ24150: + case BQ24150A: + case BQ24151: + case BQ24151A: + case BQ24152: + if (ret >= 0 && ret <= 3) + return ret; + return -1; + case BQ24153: + case BQ24153A: + case BQ24156: + case BQ24156A: + case BQ24157S: + case BQ24158: + if (ret == 3) + return 0; + else if (ret == 1) + return 1; + return -1; + case BQ24155: + if (ret == 3) + return 3; + return -1; + case BQUNKNOWN: + return -1; + } + + return -1; +} + +/* return chip vender code */ +static int bq2415x_get_vender_code(struct bq2415x_device *bq) +{ + int ret; + + ret = bq2415x_exec_command(bq, BQ2415X_VENDER_CODE); + if (ret < 0) + return 0; + + /* convert to binary */ + return (ret & 0x1) + + ((ret >> 1) & 0x1) * 10 + + ((ret >> 2) & 0x1) * 100; +} + +/* reset all chip registers to default state */ +static void bq2415x_reset_chip(struct bq2415x_device *bq) +{ + bq2415x_i2c_write(bq, BQ2415X_REG_CURRENT, BQ2415X_RESET_CURRENT); + bq2415x_i2c_write(bq, BQ2415X_REG_VOLTAGE, BQ2415X_RESET_VOLTAGE); + bq2415x_i2c_write(bq, BQ2415X_REG_CONTROL, BQ2415X_RESET_CONTROL); + bq2415x_i2c_write(bq, BQ2415X_REG_STATUS, BQ2415X_RESET_STATUS); + bq->timer_error = NULL; +} + +/**** properties functions ****/ + +/* set current limit in mA */ +static int bq2415x_set_current_limit(struct bq2415x_device *bq, int mA) +{ + int val; + + if (mA <= 100) + val = 0; + else if (mA <= 500) + val = 1; + else if (mA <= 800) + val = 2; + else + val = 3; + + return bq2415x_i2c_write_mask(bq, BQ2415X_REG_CONTROL, val, + BQ2415X_MASK_LIMIT, BQ2415X_SHIFT_LIMIT); +} + +/* get current limit in mA */ +static int bq2415x_get_current_limit(struct bq2415x_device *bq) +{ + int ret; + + ret = bq2415x_i2c_read_mask(bq, BQ2415X_REG_CONTROL, + BQ2415X_MASK_LIMIT, BQ2415X_SHIFT_LIMIT); + if (ret < 0) + return ret; + else if (ret == 0) + return 100; + else if (ret == 1) + return 500; + else if (ret == 2) + return 800; + else if (ret == 3) + return 1800; + return -EINVAL; +} + +/* set weak battery voltage in mV */ +static int bq2415x_set_weak_battery_voltage(struct bq2415x_device *bq, int mV) +{ + int val; + + /* round to 100mV */ + if (mV <= 3400 + 50) + val = 0; + else if (mV <= 3500 + 50) + val = 1; + else if (mV <= 3600 + 50) + val = 2; + else + val = 3; + + return bq2415x_i2c_write_mask(bq, BQ2415X_REG_CONTROL, val, + BQ2415X_MASK_VLOWV, BQ2415X_SHIFT_VLOWV); +} + +/* get weak battery voltage in mV */ +static int bq2415x_get_weak_battery_voltage(struct bq2415x_device *bq) +{ + int ret; + + ret = bq2415x_i2c_read_mask(bq, BQ2415X_REG_CONTROL, + BQ2415X_MASK_VLOWV, BQ2415X_SHIFT_VLOWV); + if (ret < 0) + return ret; + return 100 * (34 + ret); +} + +/* set battery regulation voltage in mV */ +static int bq2415x_set_battery_regulation_voltage(struct bq2415x_device *bq, + int mV) +{ + int val = (mV/10 - 350) / 2; + + /* + * According to datasheet, maximum battery regulation voltage is + * 4440mV which is b101111 = 47. + */ + if (val < 0) + val = 0; + else if (val > 47) + return -EINVAL; + + return bq2415x_i2c_write_mask(bq, BQ2415X_REG_VOLTAGE, val, + BQ2415X_MASK_VO, BQ2415X_SHIFT_VO); +} + +/* get battery regulation voltage in mV */ +static int bq2415x_get_battery_regulation_voltage(struct bq2415x_device *bq) +{ + int ret = bq2415x_i2c_read_mask(bq, BQ2415X_REG_VOLTAGE, + BQ2415X_MASK_VO, BQ2415X_SHIFT_VO); + + if (ret < 0) + return ret; + return 10 * (350 + 2*ret); +} + +/* set charge current in mA (platform data must provide resistor sense) */ +static int bq2415x_set_charge_current(struct bq2415x_device *bq, int mA) +{ + int val; + + if (bq->init_data.resistor_sense <= 0) + return -EINVAL; + + val = (mA * bq->init_data.resistor_sense - 37400) / 6800; + if (val < 0) + val = 0; + else if (val > 7) + val = 7; + + return bq2415x_i2c_write_mask(bq, BQ2415X_REG_CURRENT, val, + BQ2415X_MASK_VI_CHRG | BQ2415X_MASK_RESET, + BQ2415X_SHIFT_VI_CHRG); +} + +/* get charge current in mA (platform data must provide resistor sense) */ +static int bq2415x_get_charge_current(struct bq2415x_device *bq) +{ + int ret; + + if (bq->init_data.resistor_sense <= 0) + return -EINVAL; + + ret = bq2415x_i2c_read_mask(bq, BQ2415X_REG_CURRENT, + BQ2415X_MASK_VI_CHRG, BQ2415X_SHIFT_VI_CHRG); + if (ret < 0) + return ret; + return (37400 + 6800*ret) / bq->init_data.resistor_sense; +} + +/* set termination current in mA (platform data must provide resistor sense) */ +static int bq2415x_set_termination_current(struct bq2415x_device *bq, int mA) +{ + int val; + + if (bq->init_data.resistor_sense <= 0) + return -EINVAL; + + val = (mA * bq->init_data.resistor_sense - 3400) / 3400; + if (val < 0) + val = 0; + else if (val > 7) + val = 7; + + return bq2415x_i2c_write_mask(bq, BQ2415X_REG_CURRENT, val, + BQ2415X_MASK_VI_TERM | BQ2415X_MASK_RESET, + BQ2415X_SHIFT_VI_TERM); +} + +/* get termination current in mA (platform data must provide resistor sense) */ +static int bq2415x_get_termination_current(struct bq2415x_device *bq) +{ + int ret; + + if (bq->init_data.resistor_sense <= 0) + return -EINVAL; + + ret = bq2415x_i2c_read_mask(bq, BQ2415X_REG_CURRENT, + BQ2415X_MASK_VI_TERM, BQ2415X_SHIFT_VI_TERM); + if (ret < 0) + return ret; + return (3400 + 3400*ret) / bq->init_data.resistor_sense; +} + +/* set default value of property */ +#define bq2415x_set_default_value(bq, prop) \ + do { \ + int ret = 0; \ + if (bq->init_data.prop != -1) \ + ret = bq2415x_set_##prop(bq, bq->init_data.prop); \ + if (ret < 0) \ + return ret; \ + } while (0) + +/* set default values of all properties */ +static int bq2415x_set_defaults(struct bq2415x_device *bq) +{ + bq2415x_exec_command(bq, BQ2415X_BOOST_MODE_DISABLE); + bq2415x_exec_command(bq, BQ2415X_CHARGER_DISABLE); + bq2415x_exec_command(bq, BQ2415X_CHARGE_TERMINATION_DISABLE); + + bq2415x_set_default_value(bq, current_limit); + bq2415x_set_default_value(bq, weak_battery_voltage); + bq2415x_set_default_value(bq, battery_regulation_voltage); + + if (bq->init_data.resistor_sense > 0) { + bq2415x_set_default_value(bq, charge_current); + bq2415x_set_default_value(bq, termination_current); + bq2415x_exec_command(bq, BQ2415X_CHARGE_TERMINATION_ENABLE); + } + + bq2415x_exec_command(bq, BQ2415X_CHARGER_ENABLE); + return 0; +} + +/**** charger mode functions ****/ + +/* set charger mode */ +static int bq2415x_set_mode(struct bq2415x_device *bq, enum bq2415x_mode mode) +{ + int ret = 0; + int charger = 0; + int boost = 0; + + if (mode == BQ2415X_MODE_BOOST) + boost = 1; + else if (mode != BQ2415X_MODE_OFF) + charger = 1; + + if (!charger) + ret = bq2415x_exec_command(bq, BQ2415X_CHARGER_DISABLE); + + if (!boost) + ret = bq2415x_exec_command(bq, BQ2415X_BOOST_MODE_DISABLE); + + if (ret < 0) + return ret; + + switch (mode) { + case BQ2415X_MODE_OFF: + dev_dbg(bq->dev, "changing mode to: Offline\n"); + ret = bq2415x_set_current_limit(bq, 100); + break; + case BQ2415X_MODE_NONE: + dev_dbg(bq->dev, "changing mode to: N/A\n"); + ret = bq2415x_set_current_limit(bq, 100); + break; + case BQ2415X_MODE_HOST_CHARGER: + dev_dbg(bq->dev, "changing mode to: Host/HUB charger\n"); + ret = bq2415x_set_current_limit(bq, 500); + break; + case BQ2415X_MODE_DEDICATED_CHARGER: + dev_dbg(bq->dev, "changing mode to: Dedicated charger\n"); + ret = bq2415x_set_current_limit(bq, 1800); + break; + case BQ2415X_MODE_BOOST: /* Boost mode */ + dev_dbg(bq->dev, "changing mode to: Boost\n"); + ret = bq2415x_set_current_limit(bq, 100); + break; + } + + if (ret < 0) + return ret; + + if (charger) + ret = bq2415x_exec_command(bq, BQ2415X_CHARGER_ENABLE); + else if (boost) + ret = bq2415x_exec_command(bq, BQ2415X_BOOST_MODE_ENABLE); + + if (ret < 0) + return ret; + + bq2415x_set_default_value(bq, weak_battery_voltage); + bq2415x_set_default_value(bq, battery_regulation_voltage); + + bq->mode = mode; + sysfs_notify(&bq->charger->dev.kobj, NULL, "mode"); + + return 0; + +} + +static bool bq2415x_update_reported_mode(struct bq2415x_device *bq, int mA) +{ + enum bq2415x_mode mode; + + if (mA == 0) + mode = BQ2415X_MODE_OFF; + else if (mA < 500) + mode = BQ2415X_MODE_NONE; + else if (mA < 1800) + mode = BQ2415X_MODE_HOST_CHARGER; + else + mode = BQ2415X_MODE_DEDICATED_CHARGER; + + if (bq->reported_mode == mode) + return false; + + bq->reported_mode = mode; + return true; +} + +static int bq2415x_notifier_call(struct notifier_block *nb, + unsigned long val, void *v) +{ + struct bq2415x_device *bq = + container_of(nb, struct bq2415x_device, nb); + struct power_supply *psy = v; + union power_supply_propval prop; + int ret; + + if (val != PSY_EVENT_PROP_CHANGED) + return NOTIFY_OK; + + /* Ignore event if it was not send by notify_node/notify_device */ + if (bq->notify_node) { + if (!psy->dev.parent || + psy->dev.parent->of_node != bq->notify_node) + return NOTIFY_OK; + } else if (bq->init_data.notify_device) { + if (strcmp(psy->desc->name, bq->init_data.notify_device) != 0) + return NOTIFY_OK; + } + + dev_dbg(bq->dev, "notifier call was called\n"); + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_CURRENT_MAX, + &prop); + if (ret != 0) + return NOTIFY_OK; + + if (!bq2415x_update_reported_mode(bq, prop.intval)) + return NOTIFY_OK; + + /* if automode is not enabled do not tell about reported_mode */ + if (bq->automode < 1) + return NOTIFY_OK; + + schedule_delayed_work(&bq->work, 0); + + return NOTIFY_OK; +} + +/**** timer functions ****/ + +/* enable/disable auto resetting chip timer */ +static void bq2415x_set_autotimer(struct bq2415x_device *bq, int state) +{ + mutex_lock(&bq2415x_timer_mutex); + + if (bq->autotimer == state) { + mutex_unlock(&bq2415x_timer_mutex); + return; + } + + bq->autotimer = state; + + if (state) { + schedule_delayed_work(&bq->work, BQ2415X_TIMER_TIMEOUT * HZ); + bq2415x_exec_command(bq, BQ2415X_TIMER_RESET); + bq->timer_error = NULL; + } else { + cancel_delayed_work_sync(&bq->work); + } + + mutex_unlock(&bq2415x_timer_mutex); +} + +/* called by bq2415x_timer_work on timer error */ +static void bq2415x_timer_error(struct bq2415x_device *bq, const char *msg) +{ + bq->timer_error = msg; + sysfs_notify(&bq->charger->dev.kobj, NULL, "timer"); + dev_err(bq->dev, "%s\n", msg); + if (bq->automode > 0) + bq->automode = 0; + bq2415x_set_mode(bq, BQ2415X_MODE_OFF); + bq2415x_set_autotimer(bq, 0); +} + +/* delayed work function for auto resetting chip timer */ +static void bq2415x_timer_work(struct work_struct *work) +{ + struct bq2415x_device *bq = container_of(work, struct bq2415x_device, + work.work); + int ret; + int error; + int boost; + + if (bq->automode > 0 && (bq->reported_mode != bq->mode)) { + sysfs_notify(&bq->charger->dev.kobj, NULL, "reported_mode"); + bq2415x_set_mode(bq, bq->reported_mode); + } + + if (!bq->autotimer) + return; + + ret = bq2415x_exec_command(bq, BQ2415X_TIMER_RESET); + if (ret < 0) { + bq2415x_timer_error(bq, "Resetting timer failed"); + return; + } + + boost = bq2415x_exec_command(bq, BQ2415X_BOOST_MODE_STATUS); + if (boost < 0) { + bq2415x_timer_error(bq, "Unknown error"); + return; + } + + error = bq2415x_exec_command(bq, BQ2415X_FAULT_STATUS); + if (error < 0) { + bq2415x_timer_error(bq, "Unknown error"); + return; + } + + if (boost) { + switch (error) { + /* Non fatal errors, chip is OK */ + case 0: /* No error */ + break; + case 6: /* Timer expired */ + dev_err(bq->dev, "Timer expired\n"); + break; + case 3: /* Battery voltage too low */ + dev_err(bq->dev, "Battery voltage to low\n"); + break; + + /* Fatal errors, disable and reset chip */ + case 1: /* Overvoltage protection (chip fried) */ + bq2415x_timer_error(bq, + "Overvoltage protection (chip fried)"); + return; + case 2: /* Overload */ + bq2415x_timer_error(bq, "Overload"); + return; + case 4: /* Battery overvoltage protection */ + bq2415x_timer_error(bq, + "Battery overvoltage protection"); + return; + case 5: /* Thermal shutdown (too hot) */ + bq2415x_timer_error(bq, + "Thermal shutdown (too hot)"); + return; + case 7: /* N/A */ + bq2415x_timer_error(bq, "Unknown error"); + return; + } + } else { + switch (error) { + /* Non fatal errors, chip is OK */ + case 0: /* No error */ + break; + case 2: /* Sleep mode */ + dev_err(bq->dev, "Sleep mode\n"); + break; + case 3: /* Poor input source */ + dev_err(bq->dev, "Poor input source\n"); + break; + case 6: /* Timer expired */ + dev_err(bq->dev, "Timer expired\n"); + break; + case 7: /* No battery */ + dev_err(bq->dev, "No battery\n"); + break; + + /* Fatal errors, disable and reset chip */ + case 1: /* Overvoltage protection (chip fried) */ + bq2415x_timer_error(bq, + "Overvoltage protection (chip fried)"); + return; + case 4: /* Battery overvoltage protection */ + bq2415x_timer_error(bq, + "Battery overvoltage protection"); + return; + case 5: /* Thermal shutdown (too hot) */ + bq2415x_timer_error(bq, + "Thermal shutdown (too hot)"); + return; + } + } + + schedule_delayed_work(&bq->work, BQ2415X_TIMER_TIMEOUT * HZ); +} + +/**** power supply interface code ****/ + +static enum power_supply_property bq2415x_power_supply_props[] = { + /* TODO: maybe add more power supply properties */ + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +static int bq2415x_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = bq2415x_exec_command(bq, BQ2415X_CHARGE_STATUS); + if (ret < 0) + return ret; + else if (ret == 0) /* Ready */ + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (ret == 1) /* Charge in progress */ + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (ret == 2) /* Charge done */ + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bq->model; + break; + default: + return -EINVAL; + } + return 0; +} + +static void bq2415x_power_supply_exit(struct bq2415x_device *bq) +{ + bq->autotimer = 0; + if (bq->automode > 0) + bq->automode = 0; + cancel_delayed_work_sync(&bq->work); + power_supply_unregister(bq->charger); + kfree(bq->model); +} + +/**** additional sysfs entries for power supply interface ****/ + +/* show *_status entries */ +static ssize_t bq2415x_sysfs_show_status(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + enum bq2415x_command command; + int ret; + + if (strcmp(attr->attr.name, "otg_status") == 0) + command = BQ2415X_OTG_STATUS; + else if (strcmp(attr->attr.name, "charge_status") == 0) + command = BQ2415X_CHARGE_STATUS; + else if (strcmp(attr->attr.name, "boost_status") == 0) + command = BQ2415X_BOOST_STATUS; + else if (strcmp(attr->attr.name, "fault_status") == 0) + command = BQ2415X_FAULT_STATUS; + else + return -EINVAL; + + ret = bq2415x_exec_command(bq, command); + if (ret < 0) + return ret; + return sprintf(buf, "%d\n", ret); +} + +/* + * set timer entry: + * auto - enable auto mode + * off - disable auto mode + * (other values) - reset chip timer + */ +static ssize_t bq2415x_sysfs_set_timer(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + int ret = 0; + + if (strncmp(buf, "auto", 4) == 0) + bq2415x_set_autotimer(bq, 1); + else if (strncmp(buf, "off", 3) == 0) + bq2415x_set_autotimer(bq, 0); + else + ret = bq2415x_exec_command(bq, BQ2415X_TIMER_RESET); + + if (ret < 0) + return ret; + return count; +} + +/* show timer entry (auto or off) */ +static ssize_t bq2415x_sysfs_show_timer(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + + if (bq->timer_error) + return sprintf(buf, "%s\n", bq->timer_error); + + if (bq->autotimer) + return sprintf(buf, "auto\n"); + return sprintf(buf, "off\n"); +} + +/* + * set mode entry: + * auto - if automode is supported, enable it and set mode to reported + * none - disable charger and boost mode + * host - charging mode for host/hub chargers (current limit 500mA) + * dedicated - charging mode for dedicated chargers (unlimited current limit) + * boost - disable charger and enable boost mode + */ +static ssize_t bq2415x_sysfs_set_mode(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + enum bq2415x_mode mode; + int ret = 0; + + if (strncmp(buf, "auto", 4) == 0) { + if (bq->automode < 0) + return -EINVAL; + bq->automode = 1; + mode = bq->reported_mode; + } else if (strncmp(buf, "off", 3) == 0) { + if (bq->automode > 0) + bq->automode = 0; + mode = BQ2415X_MODE_OFF; + } else if (strncmp(buf, "none", 4) == 0) { + if (bq->automode > 0) + bq->automode = 0; + mode = BQ2415X_MODE_NONE; + } else if (strncmp(buf, "host", 4) == 0) { + if (bq->automode > 0) + bq->automode = 0; + mode = BQ2415X_MODE_HOST_CHARGER; + } else if (strncmp(buf, "dedicated", 9) == 0) { + if (bq->automode > 0) + bq->automode = 0; + mode = BQ2415X_MODE_DEDICATED_CHARGER; + } else if (strncmp(buf, "boost", 5) == 0) { + if (bq->automode > 0) + bq->automode = 0; + mode = BQ2415X_MODE_BOOST; + } else if (strncmp(buf, "reset", 5) == 0) { + bq2415x_reset_chip(bq); + bq2415x_set_defaults(bq); + if (bq->automode <= 0) + return count; + bq->automode = 1; + mode = bq->reported_mode; + } else { + return -EINVAL; + } + + ret = bq2415x_set_mode(bq, mode); + if (ret < 0) + return ret; + return count; +} + +/* show mode entry (auto, none, host, dedicated or boost) */ +static ssize_t bq2415x_sysfs_show_mode(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + ssize_t ret = 0; + + if (bq->automode > 0) + ret += sprintf(buf+ret, "auto ("); + + switch (bq->mode) { + case BQ2415X_MODE_OFF: + ret += sprintf(buf+ret, "off"); + break; + case BQ2415X_MODE_NONE: + ret += sprintf(buf+ret, "none"); + break; + case BQ2415X_MODE_HOST_CHARGER: + ret += sprintf(buf+ret, "host"); + break; + case BQ2415X_MODE_DEDICATED_CHARGER: + ret += sprintf(buf+ret, "dedicated"); + break; + case BQ2415X_MODE_BOOST: + ret += sprintf(buf+ret, "boost"); + break; + } + + if (bq->automode > 0) + ret += sprintf(buf+ret, ")"); + + ret += sprintf(buf+ret, "\n"); + return ret; +} + +/* show reported_mode entry (none, host, dedicated or boost) */ +static ssize_t bq2415x_sysfs_show_reported_mode(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + + if (bq->automode < 0) + return -EINVAL; + + switch (bq->reported_mode) { + case BQ2415X_MODE_OFF: + return sprintf(buf, "off\n"); + case BQ2415X_MODE_NONE: + return sprintf(buf, "none\n"); + case BQ2415X_MODE_HOST_CHARGER: + return sprintf(buf, "host\n"); + case BQ2415X_MODE_DEDICATED_CHARGER: + return sprintf(buf, "dedicated\n"); + case BQ2415X_MODE_BOOST: + return sprintf(buf, "boost\n"); + } + + return -EINVAL; +} + +/* directly set raw value to chip register, format: 'register value' */ +static ssize_t bq2415x_sysfs_set_registers(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + ssize_t ret = 0; + unsigned int reg; + unsigned int val; + + if (sscanf(buf, "%x %x", ®, &val) != 2) + return -EINVAL; + + if (reg > 4 || val > 255) + return -EINVAL; + + ret = bq2415x_i2c_write(bq, reg, val); + if (ret < 0) + return ret; + return count; +} + +/* print value of chip register, format: 'register=value' */ +static ssize_t bq2415x_sysfs_print_reg(struct bq2415x_device *bq, + u8 reg, + char *buf) +{ + int ret = bq2415x_i2c_read(bq, reg); + + if (ret < 0) + return sprintf(buf, "%#.2x=error %d\n", reg, ret); + return sprintf(buf, "%#.2x=%#.2x\n", reg, ret); +} + +/* show all raw values of chip register, format per line: 'register=value' */ +static ssize_t bq2415x_sysfs_show_registers(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + ssize_t ret = 0; + + ret += bq2415x_sysfs_print_reg(bq, BQ2415X_REG_STATUS, buf+ret); + ret += bq2415x_sysfs_print_reg(bq, BQ2415X_REG_CONTROL, buf+ret); + ret += bq2415x_sysfs_print_reg(bq, BQ2415X_REG_VOLTAGE, buf+ret); + ret += bq2415x_sysfs_print_reg(bq, BQ2415X_REG_VENDER, buf+ret); + ret += bq2415x_sysfs_print_reg(bq, BQ2415X_REG_CURRENT, buf+ret); + return ret; +} + +/* set current and voltage limit entries (in mA or mV) */ +static ssize_t bq2415x_sysfs_set_limit(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + long val; + int ret; + + if (kstrtol(buf, 10, &val) < 0) + return -EINVAL; + + if (strcmp(attr->attr.name, "current_limit") == 0) + ret = bq2415x_set_current_limit(bq, val); + else if (strcmp(attr->attr.name, "weak_battery_voltage") == 0) + ret = bq2415x_set_weak_battery_voltage(bq, val); + else if (strcmp(attr->attr.name, "battery_regulation_voltage") == 0) + ret = bq2415x_set_battery_regulation_voltage(bq, val); + else if (strcmp(attr->attr.name, "charge_current") == 0) + ret = bq2415x_set_charge_current(bq, val); + else if (strcmp(attr->attr.name, "termination_current") == 0) + ret = bq2415x_set_termination_current(bq, val); + else + return -EINVAL; + + if (ret < 0) + return ret; + return count; +} + +/* show current and voltage limit entries (in mA or mV) */ +static ssize_t bq2415x_sysfs_show_limit(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + int ret; + + if (strcmp(attr->attr.name, "current_limit") == 0) + ret = bq2415x_get_current_limit(bq); + else if (strcmp(attr->attr.name, "weak_battery_voltage") == 0) + ret = bq2415x_get_weak_battery_voltage(bq); + else if (strcmp(attr->attr.name, "battery_regulation_voltage") == 0) + ret = bq2415x_get_battery_regulation_voltage(bq); + else if (strcmp(attr->attr.name, "charge_current") == 0) + ret = bq2415x_get_charge_current(bq); + else if (strcmp(attr->attr.name, "termination_current") == 0) + ret = bq2415x_get_termination_current(bq); + else + return -EINVAL; + + if (ret < 0) + return ret; + return sprintf(buf, "%d\n", ret); +} + +/* set *_enable entries */ +static ssize_t bq2415x_sysfs_set_enable(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + enum bq2415x_command command; + long val; + int ret; + + if (kstrtol(buf, 10, &val) < 0) + return -EINVAL; + + if (strcmp(attr->attr.name, "charge_termination_enable") == 0) + command = val ? BQ2415X_CHARGE_TERMINATION_ENABLE : + BQ2415X_CHARGE_TERMINATION_DISABLE; + else if (strcmp(attr->attr.name, "high_impedance_enable") == 0) + command = val ? BQ2415X_HIGH_IMPEDANCE_ENABLE : + BQ2415X_HIGH_IMPEDANCE_DISABLE; + else if (strcmp(attr->attr.name, "otg_pin_enable") == 0) + command = val ? BQ2415X_OTG_PIN_ENABLE : + BQ2415X_OTG_PIN_DISABLE; + else if (strcmp(attr->attr.name, "stat_pin_enable") == 0) + command = val ? BQ2415X_STAT_PIN_ENABLE : + BQ2415X_STAT_PIN_DISABLE; + else + return -EINVAL; + + ret = bq2415x_exec_command(bq, command); + if (ret < 0) + return ret; + return count; +} + +/* show *_enable entries */ +static ssize_t bq2415x_sysfs_show_enable(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq2415x_device *bq = power_supply_get_drvdata(psy); + enum bq2415x_command command; + int ret; + + if (strcmp(attr->attr.name, "charge_termination_enable") == 0) + command = BQ2415X_CHARGE_TERMINATION_STATUS; + else if (strcmp(attr->attr.name, "high_impedance_enable") == 0) + command = BQ2415X_HIGH_IMPEDANCE_STATUS; + else if (strcmp(attr->attr.name, "otg_pin_enable") == 0) + command = BQ2415X_OTG_PIN_STATUS; + else if (strcmp(attr->attr.name, "stat_pin_enable") == 0) + command = BQ2415X_STAT_PIN_STATUS; + else + return -EINVAL; + + ret = bq2415x_exec_command(bq, command); + if (ret < 0) + return ret; + return sprintf(buf, "%d\n", ret); +} + +static DEVICE_ATTR(current_limit, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_limit, bq2415x_sysfs_set_limit); +static DEVICE_ATTR(weak_battery_voltage, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_limit, bq2415x_sysfs_set_limit); +static DEVICE_ATTR(battery_regulation_voltage, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_limit, bq2415x_sysfs_set_limit); +static DEVICE_ATTR(charge_current, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_limit, bq2415x_sysfs_set_limit); +static DEVICE_ATTR(termination_current, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_limit, bq2415x_sysfs_set_limit); + +static DEVICE_ATTR(charge_termination_enable, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_enable, bq2415x_sysfs_set_enable); +static DEVICE_ATTR(high_impedance_enable, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_enable, bq2415x_sysfs_set_enable); +static DEVICE_ATTR(otg_pin_enable, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_enable, bq2415x_sysfs_set_enable); +static DEVICE_ATTR(stat_pin_enable, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_enable, bq2415x_sysfs_set_enable); + +static DEVICE_ATTR(reported_mode, S_IRUGO, + bq2415x_sysfs_show_reported_mode, NULL); +static DEVICE_ATTR(mode, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_mode, bq2415x_sysfs_set_mode); +static DEVICE_ATTR(timer, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_timer, bq2415x_sysfs_set_timer); + +static DEVICE_ATTR(registers, S_IWUSR | S_IRUGO, + bq2415x_sysfs_show_registers, bq2415x_sysfs_set_registers); + +static DEVICE_ATTR(otg_status, S_IRUGO, bq2415x_sysfs_show_status, NULL); +static DEVICE_ATTR(charge_status, S_IRUGO, bq2415x_sysfs_show_status, NULL); +static DEVICE_ATTR(boost_status, S_IRUGO, bq2415x_sysfs_show_status, NULL); +static DEVICE_ATTR(fault_status, S_IRUGO, bq2415x_sysfs_show_status, NULL); + +static struct attribute *bq2415x_sysfs_attrs[] = { + /* + * TODO: some (appropriate) of these attrs should be switched to + * use power supply class props. + */ + &dev_attr_current_limit.attr, + &dev_attr_weak_battery_voltage.attr, + &dev_attr_battery_regulation_voltage.attr, + &dev_attr_charge_current.attr, + &dev_attr_termination_current.attr, + + &dev_attr_charge_termination_enable.attr, + &dev_attr_high_impedance_enable.attr, + &dev_attr_otg_pin_enable.attr, + &dev_attr_stat_pin_enable.attr, + + &dev_attr_reported_mode.attr, + &dev_attr_mode.attr, + &dev_attr_timer.attr, + + &dev_attr_registers.attr, + + &dev_attr_otg_status.attr, + &dev_attr_charge_status.attr, + &dev_attr_boost_status.attr, + &dev_attr_fault_status.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(bq2415x_sysfs); + +static int bq2415x_power_supply_init(struct bq2415x_device *bq) +{ + int ret; + int chip; + char revstr[8]; + struct power_supply_config psy_cfg = { + .drv_data = bq, + .of_node = bq->dev->of_node, + .attr_grp = bq2415x_sysfs_groups, + }; + + bq->charger_desc.name = bq->name; + bq->charger_desc.type = POWER_SUPPLY_TYPE_USB; + bq->charger_desc.properties = bq2415x_power_supply_props; + bq->charger_desc.num_properties = + ARRAY_SIZE(bq2415x_power_supply_props); + bq->charger_desc.get_property = bq2415x_power_supply_get_property; + + ret = bq2415x_detect_chip(bq); + if (ret < 0) + chip = BQUNKNOWN; + else + chip = ret; + + ret = bq2415x_detect_revision(bq); + if (ret < 0) + strcpy(revstr, "unknown"); + else + sprintf(revstr, "1.%d", ret); + + bq->model = kasprintf(GFP_KERNEL, + "chip %s, revision %s, vender code %.3d", + bq2415x_chip_name[chip], revstr, + bq2415x_get_vender_code(bq)); + if (!bq->model) { + dev_err(bq->dev, "failed to allocate model name\n"); + return -ENOMEM; + } + + bq->charger = power_supply_register(bq->dev, &bq->charger_desc, + &psy_cfg); + if (IS_ERR(bq->charger)) { + kfree(bq->model); + return PTR_ERR(bq->charger); + } + + return 0; +} + +/* main bq2415x probe function */ +static int bq2415x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + int num; + char *name = NULL; + struct bq2415x_device *bq; + struct device_node *np = client->dev.of_node; + struct bq2415x_platform_data *pdata = client->dev.platform_data; + const struct acpi_device_id *acpi_id = NULL; + struct power_supply *notify_psy = NULL; + union power_supply_propval prop; + + if (!np && !pdata && !ACPI_HANDLE(&client->dev)) { + dev_err(&client->dev, "Neither devicetree, nor platform data, nor ACPI support\n"); + return -ENODEV; + } + + /* Get new ID for the new device */ + mutex_lock(&bq2415x_id_mutex); + num = idr_alloc(&bq2415x_id, client, 0, 0, GFP_KERNEL); + mutex_unlock(&bq2415x_id_mutex); + if (num < 0) + return num; + + if (id) { + name = kasprintf(GFP_KERNEL, "%s-%d", id->name, num); + } else if (ACPI_HANDLE(&client->dev)) { + acpi_id = + acpi_match_device(client->dev.driver->acpi_match_table, + &client->dev); + if (!acpi_id) { + dev_err(&client->dev, "failed to match device name\n"); + ret = -ENODEV; + goto error_1; + } + name = kasprintf(GFP_KERNEL, "%s-%d", acpi_id->id, num); + } + if (!name) { + dev_err(&client->dev, "failed to allocate device name\n"); + ret = -ENOMEM; + goto error_1; + } + + bq = devm_kzalloc(&client->dev, sizeof(*bq), GFP_KERNEL); + if (!bq) { + ret = -ENOMEM; + goto error_2; + } + + i2c_set_clientdata(client, bq); + + bq->id = num; + bq->dev = &client->dev; + if (id) + bq->chip = id->driver_data; + else if (ACPI_HANDLE(bq->dev)) + bq->chip = acpi_id->driver_data; + bq->name = name; + bq->mode = BQ2415X_MODE_OFF; + bq->reported_mode = BQ2415X_MODE_OFF; + bq->autotimer = 0; + bq->automode = 0; + + if (np || ACPI_HANDLE(bq->dev)) { + ret = device_property_read_u32(bq->dev, + "ti,current-limit", + &bq->init_data.current_limit); + if (ret) + goto error_2; + ret = device_property_read_u32(bq->dev, + "ti,weak-battery-voltage", + &bq->init_data.weak_battery_voltage); + if (ret) + goto error_2; + ret = device_property_read_u32(bq->dev, + "ti,battery-regulation-voltage", + &bq->init_data.battery_regulation_voltage); + if (ret) + goto error_2; + ret = device_property_read_u32(bq->dev, + "ti,charge-current", + &bq->init_data.charge_current); + if (ret) + goto error_2; + ret = device_property_read_u32(bq->dev, + "ti,termination-current", + &bq->init_data.termination_current); + if (ret) + goto error_2; + ret = device_property_read_u32(bq->dev, + "ti,resistor-sense", + &bq->init_data.resistor_sense); + if (ret) + goto error_2; + if (np) + bq->notify_node = of_parse_phandle(np, + "ti,usb-charger-detection", 0); + } else { + memcpy(&bq->init_data, pdata, sizeof(bq->init_data)); + } + + bq2415x_reset_chip(bq); + + ret = bq2415x_power_supply_init(bq); + if (ret) { + dev_err(bq->dev, "failed to register power supply: %d\n", ret); + goto error_2; + } + + ret = bq2415x_set_defaults(bq); + if (ret) { + dev_err(bq->dev, "failed to set default values: %d\n", ret); + goto error_3; + } + + if (bq->notify_node || bq->init_data.notify_device) { + bq->nb.notifier_call = bq2415x_notifier_call; + ret = power_supply_reg_notifier(&bq->nb); + if (ret) { + dev_err(bq->dev, "failed to reg notifier: %d\n", ret); + goto error_3; + } + + bq->automode = 1; + dev_info(bq->dev, "automode supported, waiting for events\n"); + } else { + bq->automode = -1; + dev_info(bq->dev, "automode not supported\n"); + } + + /* Query for initial reported_mode and set it */ + if (bq->nb.notifier_call) { + if (np) { + notify_psy = power_supply_get_by_phandle(np, + "ti,usb-charger-detection"); + if (IS_ERR(notify_psy)) + notify_psy = NULL; + } else if (bq->init_data.notify_device) { + notify_psy = power_supply_get_by_name( + bq->init_data.notify_device); + } + } + if (notify_psy) { + ret = power_supply_get_property(notify_psy, + POWER_SUPPLY_PROP_CURRENT_MAX, &prop); + power_supply_put(notify_psy); + + if (ret == 0) { + bq2415x_update_reported_mode(bq, prop.intval); + bq2415x_set_mode(bq, bq->reported_mode); + } + } + + INIT_DELAYED_WORK(&bq->work, bq2415x_timer_work); + bq2415x_set_autotimer(bq, 1); + + dev_info(bq->dev, "driver registered\n"); + return 0; + +error_3: + bq2415x_power_supply_exit(bq); +error_2: + if (bq) + of_node_put(bq->notify_node); + kfree(name); +error_1: + mutex_lock(&bq2415x_id_mutex); + idr_remove(&bq2415x_id, num); + mutex_unlock(&bq2415x_id_mutex); + + return ret; +} + +/* main bq2415x remove function */ + +static int bq2415x_remove(struct i2c_client *client) +{ + struct bq2415x_device *bq = i2c_get_clientdata(client); + + if (bq->nb.notifier_call) + power_supply_unreg_notifier(&bq->nb); + + of_node_put(bq->notify_node); + bq2415x_power_supply_exit(bq); + + bq2415x_reset_chip(bq); + + mutex_lock(&bq2415x_id_mutex); + idr_remove(&bq2415x_id, bq->id); + mutex_unlock(&bq2415x_id_mutex); + + dev_info(bq->dev, "driver unregistered\n"); + + kfree(bq->name); + + return 0; +} + +static const struct i2c_device_id bq2415x_i2c_id_table[] = { + { "bq2415x", BQUNKNOWN }, + { "bq24150", BQ24150 }, + { "bq24150a", BQ24150A }, + { "bq24151", BQ24151 }, + { "bq24151a", BQ24151A }, + { "bq24152", BQ24152 }, + { "bq24153", BQ24153 }, + { "bq24153a", BQ24153A }, + { "bq24155", BQ24155 }, + { "bq24156", BQ24156 }, + { "bq24156a", BQ24156A }, + { "bq24157s", BQ24157S }, + { "bq24158", BQ24158 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq2415x_i2c_id_table); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id bq2415x_i2c_acpi_match[] = { + { "BQ2415X", BQUNKNOWN }, + { "BQ241500", BQ24150 }, + { "BQA24150", BQ24150A }, + { "BQ241510", BQ24151 }, + { "BQA24151", BQ24151A }, + { "BQ241520", BQ24152 }, + { "BQ241530", BQ24153 }, + { "BQA24153", BQ24153A }, + { "BQ241550", BQ24155 }, + { "BQ241560", BQ24156 }, + { "BQA24156", BQ24156A }, + { "BQS24157", BQ24157S }, + { "BQ241580", BQ24158 }, + {}, +}; +MODULE_DEVICE_TABLE(acpi, bq2415x_i2c_acpi_match); +#endif + +#ifdef CONFIG_OF +static const struct of_device_id bq2415x_of_match_table[] = { + { .compatible = "ti,bq24150" }, + { .compatible = "ti,bq24150a" }, + { .compatible = "ti,bq24151" }, + { .compatible = "ti,bq24151a" }, + { .compatible = "ti,bq24152" }, + { .compatible = "ti,bq24153" }, + { .compatible = "ti,bq24153a" }, + { .compatible = "ti,bq24155" }, + { .compatible = "ti,bq24156" }, + { .compatible = "ti,bq24156a" }, + { .compatible = "ti,bq24157s" }, + { .compatible = "ti,bq24158" }, + {}, +}; +MODULE_DEVICE_TABLE(of, bq2415x_of_match_table); +#endif + +static struct i2c_driver bq2415x_driver = { + .driver = { + .name = "bq2415x-charger", + .of_match_table = of_match_ptr(bq2415x_of_match_table), + .acpi_match_table = ACPI_PTR(bq2415x_i2c_acpi_match), + }, + .probe = bq2415x_probe, + .remove = bq2415x_remove, + .id_table = bq2415x_i2c_id_table, +}; +module_i2c_driver(bq2415x_driver); + +MODULE_AUTHOR("Pali Rohár <pali@kernel.org>"); +MODULE_DESCRIPTION("bq2415x charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bq24190_charger.c b/drivers/power/supply/bq24190_charger.c new file mode 100644 index 000000000..5769b3685 --- /dev/null +++ b/drivers/power/supply/bq24190_charger.c @@ -0,0 +1,1988 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for the TI bq24190 battery charger. + * + * Author: Mark A. Greer <mgreer@animalcreek.com> + */ + +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/of_irq.h> +#include <linux/of_device.h> +#include <linux/pm_runtime.h> +#include <linux/power_supply.h> +#include <linux/power/bq24190_charger.h> +#include <linux/regulator/driver.h> +#include <linux/regulator/machine.h> +#include <linux/workqueue.h> +#include <linux/gpio.h> +#include <linux/i2c.h> +#include <linux/extcon-provider.h> + +#define BQ24190_MANUFACTURER "Texas Instruments" + +#define BQ24190_REG_ISC 0x00 /* Input Source Control */ +#define BQ24190_REG_ISC_EN_HIZ_MASK BIT(7) +#define BQ24190_REG_ISC_EN_HIZ_SHIFT 7 +#define BQ24190_REG_ISC_VINDPM_MASK (BIT(6) | BIT(5) | BIT(4) | \ + BIT(3)) +#define BQ24190_REG_ISC_VINDPM_SHIFT 3 +#define BQ24190_REG_ISC_IINLIM_MASK (BIT(2) | BIT(1) | BIT(0)) +#define BQ24190_REG_ISC_IINLIM_SHIFT 0 + +#define BQ24190_REG_POC 0x01 /* Power-On Configuration */ +#define BQ24190_REG_POC_RESET_MASK BIT(7) +#define BQ24190_REG_POC_RESET_SHIFT 7 +#define BQ24190_REG_POC_WDT_RESET_MASK BIT(6) +#define BQ24190_REG_POC_WDT_RESET_SHIFT 6 +#define BQ24190_REG_POC_CHG_CONFIG_MASK (BIT(5) | BIT(4)) +#define BQ24190_REG_POC_CHG_CONFIG_SHIFT 4 +#define BQ24190_REG_POC_CHG_CONFIG_DISABLE 0x0 +#define BQ24190_REG_POC_CHG_CONFIG_CHARGE 0x1 +#define BQ24190_REG_POC_CHG_CONFIG_OTG 0x2 +#define BQ24190_REG_POC_CHG_CONFIG_OTG_ALT 0x3 +#define BQ24190_REG_POC_SYS_MIN_MASK (BIT(3) | BIT(2) | BIT(1)) +#define BQ24190_REG_POC_SYS_MIN_SHIFT 1 +#define BQ24190_REG_POC_SYS_MIN_MIN 3000 +#define BQ24190_REG_POC_SYS_MIN_MAX 3700 +#define BQ24190_REG_POC_BOOST_LIM_MASK BIT(0) +#define BQ24190_REG_POC_BOOST_LIM_SHIFT 0 + +#define BQ24190_REG_CCC 0x02 /* Charge Current Control */ +#define BQ24190_REG_CCC_ICHG_MASK (BIT(7) | BIT(6) | BIT(5) | \ + BIT(4) | BIT(3) | BIT(2)) +#define BQ24190_REG_CCC_ICHG_SHIFT 2 +#define BQ24190_REG_CCC_FORCE_20PCT_MASK BIT(0) +#define BQ24190_REG_CCC_FORCE_20PCT_SHIFT 0 + +#define BQ24190_REG_PCTCC 0x03 /* Pre-charge/Termination Current Cntl */ +#define BQ24190_REG_PCTCC_IPRECHG_MASK (BIT(7) | BIT(6) | BIT(5) | \ + BIT(4)) +#define BQ24190_REG_PCTCC_IPRECHG_SHIFT 4 +#define BQ24190_REG_PCTCC_IPRECHG_MIN 128 +#define BQ24190_REG_PCTCC_IPRECHG_MAX 2048 +#define BQ24190_REG_PCTCC_ITERM_MASK (BIT(3) | BIT(2) | BIT(1) | \ + BIT(0)) +#define BQ24190_REG_PCTCC_ITERM_SHIFT 0 +#define BQ24190_REG_PCTCC_ITERM_MIN 128 +#define BQ24190_REG_PCTCC_ITERM_MAX 2048 + +#define BQ24190_REG_CVC 0x04 /* Charge Voltage Control */ +#define BQ24190_REG_CVC_VREG_MASK (BIT(7) | BIT(6) | BIT(5) | \ + BIT(4) | BIT(3) | BIT(2)) +#define BQ24190_REG_CVC_VREG_SHIFT 2 +#define BQ24190_REG_CVC_BATLOWV_MASK BIT(1) +#define BQ24190_REG_CVC_BATLOWV_SHIFT 1 +#define BQ24190_REG_CVC_VRECHG_MASK BIT(0) +#define BQ24190_REG_CVC_VRECHG_SHIFT 0 + +#define BQ24190_REG_CTTC 0x05 /* Charge Term/Timer Control */ +#define BQ24190_REG_CTTC_EN_TERM_MASK BIT(7) +#define BQ24190_REG_CTTC_EN_TERM_SHIFT 7 +#define BQ24190_REG_CTTC_TERM_STAT_MASK BIT(6) +#define BQ24190_REG_CTTC_TERM_STAT_SHIFT 6 +#define BQ24190_REG_CTTC_WATCHDOG_MASK (BIT(5) | BIT(4)) +#define BQ24190_REG_CTTC_WATCHDOG_SHIFT 4 +#define BQ24190_REG_CTTC_EN_TIMER_MASK BIT(3) +#define BQ24190_REG_CTTC_EN_TIMER_SHIFT 3 +#define BQ24190_REG_CTTC_CHG_TIMER_MASK (BIT(2) | BIT(1)) +#define BQ24190_REG_CTTC_CHG_TIMER_SHIFT 1 +#define BQ24190_REG_CTTC_JEITA_ISET_MASK BIT(0) +#define BQ24190_REG_CTTC_JEITA_ISET_SHIFT 0 + +#define BQ24190_REG_ICTRC 0x06 /* IR Comp/Thermal Regulation Control */ +#define BQ24190_REG_ICTRC_BAT_COMP_MASK (BIT(7) | BIT(6) | BIT(5)) +#define BQ24190_REG_ICTRC_BAT_COMP_SHIFT 5 +#define BQ24190_REG_ICTRC_VCLAMP_MASK (BIT(4) | BIT(3) | BIT(2)) +#define BQ24190_REG_ICTRC_VCLAMP_SHIFT 2 +#define BQ24190_REG_ICTRC_TREG_MASK (BIT(1) | BIT(0)) +#define BQ24190_REG_ICTRC_TREG_SHIFT 0 + +#define BQ24190_REG_MOC 0x07 /* Misc. Operation Control */ +#define BQ24190_REG_MOC_DPDM_EN_MASK BIT(7) +#define BQ24190_REG_MOC_DPDM_EN_SHIFT 7 +#define BQ24190_REG_MOC_TMR2X_EN_MASK BIT(6) +#define BQ24190_REG_MOC_TMR2X_EN_SHIFT 6 +#define BQ24190_REG_MOC_BATFET_DISABLE_MASK BIT(5) +#define BQ24190_REG_MOC_BATFET_DISABLE_SHIFT 5 +#define BQ24190_REG_MOC_JEITA_VSET_MASK BIT(4) +#define BQ24190_REG_MOC_JEITA_VSET_SHIFT 4 +#define BQ24190_REG_MOC_INT_MASK_MASK (BIT(1) | BIT(0)) +#define BQ24190_REG_MOC_INT_MASK_SHIFT 0 + +#define BQ24190_REG_SS 0x08 /* System Status */ +#define BQ24190_REG_SS_VBUS_STAT_MASK (BIT(7) | BIT(6)) +#define BQ24190_REG_SS_VBUS_STAT_SHIFT 6 +#define BQ24190_REG_SS_CHRG_STAT_MASK (BIT(5) | BIT(4)) +#define BQ24190_REG_SS_CHRG_STAT_SHIFT 4 +#define BQ24190_REG_SS_DPM_STAT_MASK BIT(3) +#define BQ24190_REG_SS_DPM_STAT_SHIFT 3 +#define BQ24190_REG_SS_PG_STAT_MASK BIT(2) +#define BQ24190_REG_SS_PG_STAT_SHIFT 2 +#define BQ24190_REG_SS_THERM_STAT_MASK BIT(1) +#define BQ24190_REG_SS_THERM_STAT_SHIFT 1 +#define BQ24190_REG_SS_VSYS_STAT_MASK BIT(0) +#define BQ24190_REG_SS_VSYS_STAT_SHIFT 0 + +#define BQ24190_REG_F 0x09 /* Fault */ +#define BQ24190_REG_F_WATCHDOG_FAULT_MASK BIT(7) +#define BQ24190_REG_F_WATCHDOG_FAULT_SHIFT 7 +#define BQ24190_REG_F_BOOST_FAULT_MASK BIT(6) +#define BQ24190_REG_F_BOOST_FAULT_SHIFT 6 +#define BQ24190_REG_F_CHRG_FAULT_MASK (BIT(5) | BIT(4)) +#define BQ24190_REG_F_CHRG_FAULT_SHIFT 4 +#define BQ24190_REG_F_BAT_FAULT_MASK BIT(3) +#define BQ24190_REG_F_BAT_FAULT_SHIFT 3 +#define BQ24190_REG_F_NTC_FAULT_MASK (BIT(2) | BIT(1) | BIT(0)) +#define BQ24190_REG_F_NTC_FAULT_SHIFT 0 + +#define BQ24190_REG_VPRS 0x0A /* Vendor/Part/Revision Status */ +#define BQ24190_REG_VPRS_PN_MASK (BIT(5) | BIT(4) | BIT(3)) +#define BQ24190_REG_VPRS_PN_SHIFT 3 +#define BQ24190_REG_VPRS_PN_24190 0x4 +#define BQ24190_REG_VPRS_PN_24192 0x5 /* Also 24193, 24196 */ +#define BQ24190_REG_VPRS_PN_24192I 0x3 +#define BQ24190_REG_VPRS_TS_PROFILE_MASK BIT(2) +#define BQ24190_REG_VPRS_TS_PROFILE_SHIFT 2 +#define BQ24190_REG_VPRS_DEV_REG_MASK (BIT(1) | BIT(0)) +#define BQ24190_REG_VPRS_DEV_REG_SHIFT 0 + +/* + * The FAULT register is latched by the bq24190 (except for NTC_FAULT) + * so the first read after a fault returns the latched value and subsequent + * reads return the current value. In order to return the fault status + * to the user, have the interrupt handler save the reg's value and retrieve + * it in the appropriate health/status routine. + */ +struct bq24190_dev_info { + struct i2c_client *client; + struct device *dev; + struct extcon_dev *edev; + struct power_supply *charger; + struct power_supply *battery; + struct delayed_work input_current_limit_work; + char model_name[I2C_NAME_SIZE]; + bool initialized; + bool irq_event; + u16 sys_min; + u16 iprechg; + u16 iterm; + struct mutex f_reg_lock; + u8 f_reg; + u8 ss_reg; + u8 watchdog; +}; + +static const unsigned int bq24190_usb_extcon_cable[] = { + EXTCON_USB, + EXTCON_NONE, +}; + +/* + * The tables below provide a 2-way mapping for the value that goes in + * the register field and the real-world value that it represents. + * The index of the array is the value that goes in the register; the + * number at that index in the array is the real-world value that it + * represents. + */ + +/* REG00[2:0] (IINLIM) in uAh */ +static const int bq24190_isc_iinlim_values[] = { + 100000, 150000, 500000, 900000, 1200000, 1500000, 2000000, 3000000 +}; + +/* REG02[7:2] (ICHG) in uAh */ +static const int bq24190_ccc_ichg_values[] = { + 512000, 576000, 640000, 704000, 768000, 832000, 896000, 960000, + 1024000, 1088000, 1152000, 1216000, 1280000, 1344000, 1408000, 1472000, + 1536000, 1600000, 1664000, 1728000, 1792000, 1856000, 1920000, 1984000, + 2048000, 2112000, 2176000, 2240000, 2304000, 2368000, 2432000, 2496000, + 2560000, 2624000, 2688000, 2752000, 2816000, 2880000, 2944000, 3008000, + 3072000, 3136000, 3200000, 3264000, 3328000, 3392000, 3456000, 3520000, + 3584000, 3648000, 3712000, 3776000, 3840000, 3904000, 3968000, 4032000, + 4096000, 4160000, 4224000, 4288000, 4352000, 4416000, 4480000, 4544000 +}; + +/* REG04[7:2] (VREG) in uV */ +static const int bq24190_cvc_vreg_values[] = { + 3504000, 3520000, 3536000, 3552000, 3568000, 3584000, 3600000, 3616000, + 3632000, 3648000, 3664000, 3680000, 3696000, 3712000, 3728000, 3744000, + 3760000, 3776000, 3792000, 3808000, 3824000, 3840000, 3856000, 3872000, + 3888000, 3904000, 3920000, 3936000, 3952000, 3968000, 3984000, 4000000, + 4016000, 4032000, 4048000, 4064000, 4080000, 4096000, 4112000, 4128000, + 4144000, 4160000, 4176000, 4192000, 4208000, 4224000, 4240000, 4256000, + 4272000, 4288000, 4304000, 4320000, 4336000, 4352000, 4368000, 4384000, + 4400000 +}; + +/* REG06[1:0] (TREG) in tenths of degrees Celsius */ +static const int bq24190_ictrc_treg_values[] = { + 600, 800, 1000, 1200 +}; + +/* + * Return the index in 'tbl' of greatest value that is less than or equal to + * 'val'. The index range returned is 0 to 'tbl_size' - 1. Assumes that + * the values in 'tbl' are sorted from smallest to largest and 'tbl_size' + * is less than 2^8. + */ +static u8 bq24190_find_idx(const int tbl[], int tbl_size, int v) +{ + int i; + + for (i = 1; i < tbl_size; i++) + if (v < tbl[i]) + break; + + return i - 1; +} + +/* Basic driver I/O routines */ + +static int bq24190_read(struct bq24190_dev_info *bdi, u8 reg, u8 *data) +{ + int ret; + + ret = i2c_smbus_read_byte_data(bdi->client, reg); + if (ret < 0) + return ret; + + *data = ret; + return 0; +} + +static int bq24190_write(struct bq24190_dev_info *bdi, u8 reg, u8 data) +{ + return i2c_smbus_write_byte_data(bdi->client, reg, data); +} + +static int bq24190_read_mask(struct bq24190_dev_info *bdi, u8 reg, + u8 mask, u8 shift, u8 *data) +{ + u8 v; + int ret; + + ret = bq24190_read(bdi, reg, &v); + if (ret < 0) + return ret; + + v &= mask; + v >>= shift; + *data = v; + + return 0; +} + +static int bq24190_write_mask(struct bq24190_dev_info *bdi, u8 reg, + u8 mask, u8 shift, u8 data) +{ + u8 v; + int ret; + + ret = bq24190_read(bdi, reg, &v); + if (ret < 0) + return ret; + + v &= ~mask; + v |= ((data << shift) & mask); + + return bq24190_write(bdi, reg, v); +} + +static int bq24190_get_field_val(struct bq24190_dev_info *bdi, + u8 reg, u8 mask, u8 shift, + const int tbl[], int tbl_size, + int *val) +{ + u8 v; + int ret; + + ret = bq24190_read_mask(bdi, reg, mask, shift, &v); + if (ret < 0) + return ret; + + v = (v >= tbl_size) ? (tbl_size - 1) : v; + *val = tbl[v]; + + return 0; +} + +static int bq24190_set_field_val(struct bq24190_dev_info *bdi, + u8 reg, u8 mask, u8 shift, + const int tbl[], int tbl_size, + int val) +{ + u8 idx; + + idx = bq24190_find_idx(tbl, tbl_size, val); + + return bq24190_write_mask(bdi, reg, mask, shift, idx); +} + +#ifdef CONFIG_SYSFS +/* + * There are a numerous options that are configurable on the bq24190 + * that go well beyond what the power_supply properties provide access to. + * Provide sysfs access to them so they can be examined and possibly modified + * on the fly. They will be provided for the charger power_supply object only + * and will be prefixed by 'f_' to make them easier to recognize. + */ + +#define BQ24190_SYSFS_FIELD(_name, r, f, m, store) \ +{ \ + .attr = __ATTR(f_##_name, m, bq24190_sysfs_show, store), \ + .reg = BQ24190_REG_##r, \ + .mask = BQ24190_REG_##r##_##f##_MASK, \ + .shift = BQ24190_REG_##r##_##f##_SHIFT, \ +} + +#define BQ24190_SYSFS_FIELD_RW(_name, r, f) \ + BQ24190_SYSFS_FIELD(_name, r, f, S_IWUSR | S_IRUGO, \ + bq24190_sysfs_store) + +#define BQ24190_SYSFS_FIELD_RO(_name, r, f) \ + BQ24190_SYSFS_FIELD(_name, r, f, S_IRUGO, NULL) + +static ssize_t bq24190_sysfs_show(struct device *dev, + struct device_attribute *attr, char *buf); +static ssize_t bq24190_sysfs_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count); + +struct bq24190_sysfs_field_info { + struct device_attribute attr; + u8 reg; + u8 mask; + u8 shift; +}; + +/* On i386 ptrace-abi.h defines SS that breaks the macro calls below. */ +#undef SS + +static struct bq24190_sysfs_field_info bq24190_sysfs_field_tbl[] = { + /* sysfs name reg field in reg */ + BQ24190_SYSFS_FIELD_RW(en_hiz, ISC, EN_HIZ), + BQ24190_SYSFS_FIELD_RW(vindpm, ISC, VINDPM), + BQ24190_SYSFS_FIELD_RW(iinlim, ISC, IINLIM), + BQ24190_SYSFS_FIELD_RW(chg_config, POC, CHG_CONFIG), + BQ24190_SYSFS_FIELD_RW(sys_min, POC, SYS_MIN), + BQ24190_SYSFS_FIELD_RW(boost_lim, POC, BOOST_LIM), + BQ24190_SYSFS_FIELD_RW(ichg, CCC, ICHG), + BQ24190_SYSFS_FIELD_RW(force_20_pct, CCC, FORCE_20PCT), + BQ24190_SYSFS_FIELD_RW(iprechg, PCTCC, IPRECHG), + BQ24190_SYSFS_FIELD_RW(iterm, PCTCC, ITERM), + BQ24190_SYSFS_FIELD_RW(vreg, CVC, VREG), + BQ24190_SYSFS_FIELD_RW(batlowv, CVC, BATLOWV), + BQ24190_SYSFS_FIELD_RW(vrechg, CVC, VRECHG), + BQ24190_SYSFS_FIELD_RW(en_term, CTTC, EN_TERM), + BQ24190_SYSFS_FIELD_RW(term_stat, CTTC, TERM_STAT), + BQ24190_SYSFS_FIELD_RO(watchdog, CTTC, WATCHDOG), + BQ24190_SYSFS_FIELD_RW(en_timer, CTTC, EN_TIMER), + BQ24190_SYSFS_FIELD_RW(chg_timer, CTTC, CHG_TIMER), + BQ24190_SYSFS_FIELD_RW(jeta_iset, CTTC, JEITA_ISET), + BQ24190_SYSFS_FIELD_RW(bat_comp, ICTRC, BAT_COMP), + BQ24190_SYSFS_FIELD_RW(vclamp, ICTRC, VCLAMP), + BQ24190_SYSFS_FIELD_RW(treg, ICTRC, TREG), + BQ24190_SYSFS_FIELD_RW(dpdm_en, MOC, DPDM_EN), + BQ24190_SYSFS_FIELD_RW(tmr2x_en, MOC, TMR2X_EN), + BQ24190_SYSFS_FIELD_RW(batfet_disable, MOC, BATFET_DISABLE), + BQ24190_SYSFS_FIELD_RW(jeita_vset, MOC, JEITA_VSET), + BQ24190_SYSFS_FIELD_RO(int_mask, MOC, INT_MASK), + BQ24190_SYSFS_FIELD_RO(vbus_stat, SS, VBUS_STAT), + BQ24190_SYSFS_FIELD_RO(chrg_stat, SS, CHRG_STAT), + BQ24190_SYSFS_FIELD_RO(dpm_stat, SS, DPM_STAT), + BQ24190_SYSFS_FIELD_RO(pg_stat, SS, PG_STAT), + BQ24190_SYSFS_FIELD_RO(therm_stat, SS, THERM_STAT), + BQ24190_SYSFS_FIELD_RO(vsys_stat, SS, VSYS_STAT), + BQ24190_SYSFS_FIELD_RO(watchdog_fault, F, WATCHDOG_FAULT), + BQ24190_SYSFS_FIELD_RO(boost_fault, F, BOOST_FAULT), + BQ24190_SYSFS_FIELD_RO(chrg_fault, F, CHRG_FAULT), + BQ24190_SYSFS_FIELD_RO(bat_fault, F, BAT_FAULT), + BQ24190_SYSFS_FIELD_RO(ntc_fault, F, NTC_FAULT), + BQ24190_SYSFS_FIELD_RO(pn, VPRS, PN), + BQ24190_SYSFS_FIELD_RO(ts_profile, VPRS, TS_PROFILE), + BQ24190_SYSFS_FIELD_RO(dev_reg, VPRS, DEV_REG), +}; + +static struct attribute * + bq24190_sysfs_attrs[ARRAY_SIZE(bq24190_sysfs_field_tbl) + 1]; + +ATTRIBUTE_GROUPS(bq24190_sysfs); + +static void bq24190_sysfs_init_attrs(void) +{ + int i, limit = ARRAY_SIZE(bq24190_sysfs_field_tbl); + + for (i = 0; i < limit; i++) + bq24190_sysfs_attrs[i] = &bq24190_sysfs_field_tbl[i].attr.attr; + + bq24190_sysfs_attrs[limit] = NULL; /* Has additional entry for this */ +} + +static struct bq24190_sysfs_field_info *bq24190_sysfs_field_lookup( + const char *name) +{ + int i, limit = ARRAY_SIZE(bq24190_sysfs_field_tbl); + + for (i = 0; i < limit; i++) + if (!strcmp(name, bq24190_sysfs_field_tbl[i].attr.attr.name)) + break; + + if (i >= limit) + return NULL; + + return &bq24190_sysfs_field_tbl[i]; +} + +static ssize_t bq24190_sysfs_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + struct bq24190_sysfs_field_info *info; + ssize_t count; + int ret; + u8 v; + + info = bq24190_sysfs_field_lookup(attr->attr.name); + if (!info) + return -EINVAL; + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + ret = bq24190_read_mask(bdi, info->reg, info->mask, info->shift, &v); + if (ret) + count = ret; + else + count = scnprintf(buf, PAGE_SIZE, "%hhx\n", v); + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return count; +} + +static ssize_t bq24190_sysfs_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + struct bq24190_sysfs_field_info *info; + int ret; + u8 v; + + info = bq24190_sysfs_field_lookup(attr->attr.name); + if (!info) + return -EINVAL; + + ret = kstrtou8(buf, 0, &v); + if (ret < 0) + return ret; + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + ret = bq24190_write_mask(bdi, info->reg, info->mask, info->shift, v); + if (ret) + count = ret; + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return count; +} +#endif + +#ifdef CONFIG_REGULATOR +static int bq24190_set_charge_mode(struct regulator_dev *dev, u8 val) +{ + struct bq24190_dev_info *bdi = rdev_get_drvdata(dev); + int ret; + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) { + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", ret); + return ret; + } + + ret = bq24190_write_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_CHG_CONFIG_MASK, + BQ24190_REG_POC_CHG_CONFIG_SHIFT, val); + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return ret; +} + +static int bq24190_vbus_enable(struct regulator_dev *dev) +{ + return bq24190_set_charge_mode(dev, BQ24190_REG_POC_CHG_CONFIG_OTG); +} + +static int bq24190_vbus_disable(struct regulator_dev *dev) +{ + return bq24190_set_charge_mode(dev, BQ24190_REG_POC_CHG_CONFIG_CHARGE); +} + +static int bq24190_vbus_is_enabled(struct regulator_dev *dev) +{ + struct bq24190_dev_info *bdi = rdev_get_drvdata(dev); + int ret; + u8 val; + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) { + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", ret); + return ret; + } + + ret = bq24190_read_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_CHG_CONFIG_MASK, + BQ24190_REG_POC_CHG_CONFIG_SHIFT, &val); + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + if (ret) + return ret; + + return (val == BQ24190_REG_POC_CHG_CONFIG_OTG || + val == BQ24190_REG_POC_CHG_CONFIG_OTG_ALT); +} + +static const struct regulator_ops bq24190_vbus_ops = { + .enable = bq24190_vbus_enable, + .disable = bq24190_vbus_disable, + .is_enabled = bq24190_vbus_is_enabled, +}; + +static const struct regulator_desc bq24190_vbus_desc = { + .name = "usb_otg_vbus", + .of_match = "usb-otg-vbus", + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .ops = &bq24190_vbus_ops, + .fixed_uV = 5000000, + .n_voltages = 1, +}; + +static const struct regulator_init_data bq24190_vbus_init_data = { + .constraints = { + .valid_ops_mask = REGULATOR_CHANGE_STATUS, + }, +}; + +static int bq24190_register_vbus_regulator(struct bq24190_dev_info *bdi) +{ + struct bq24190_platform_data *pdata = bdi->dev->platform_data; + struct regulator_config cfg = { }; + struct regulator_dev *reg; + int ret = 0; + + cfg.dev = bdi->dev; + if (pdata && pdata->regulator_init_data) + cfg.init_data = pdata->regulator_init_data; + else + cfg.init_data = &bq24190_vbus_init_data; + cfg.driver_data = bdi; + reg = devm_regulator_register(bdi->dev, &bq24190_vbus_desc, &cfg); + if (IS_ERR(reg)) { + ret = PTR_ERR(reg); + dev_err(bdi->dev, "Can't register regulator: %d\n", ret); + } + + return ret; +} +#else +static int bq24190_register_vbus_regulator(struct bq24190_dev_info *bdi) +{ + return 0; +} +#endif + +static int bq24190_set_config(struct bq24190_dev_info *bdi) +{ + int ret; + u8 v; + + ret = bq24190_read(bdi, BQ24190_REG_CTTC, &v); + if (ret < 0) + return ret; + + bdi->watchdog = ((v & BQ24190_REG_CTTC_WATCHDOG_MASK) >> + BQ24190_REG_CTTC_WATCHDOG_SHIFT); + + /* + * According to the "Host Mode and default Mode" section of the + * manual, a write to any register causes the bq24190 to switch + * from default mode to host mode. It will switch back to default + * mode after a WDT timeout unless the WDT is turned off as well. + * So, by simply turning off the WDT, we accomplish both with the + * same write. + */ + v &= ~BQ24190_REG_CTTC_WATCHDOG_MASK; + + ret = bq24190_write(bdi, BQ24190_REG_CTTC, v); + if (ret < 0) + return ret; + + if (bdi->sys_min) { + v = bdi->sys_min / 100 - 30; // manual section 9.5.1.2, table 9 + ret = bq24190_write_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_SYS_MIN_MASK, + BQ24190_REG_POC_SYS_MIN_SHIFT, + v); + if (ret < 0) + return ret; + } + + if (bdi->iprechg) { + v = bdi->iprechg / 128 - 1; // manual section 9.5.1.4, table 11 + ret = bq24190_write_mask(bdi, BQ24190_REG_PCTCC, + BQ24190_REG_PCTCC_IPRECHG_MASK, + BQ24190_REG_PCTCC_IPRECHG_SHIFT, + v); + if (ret < 0) + return ret; + } + + if (bdi->iterm) { + v = bdi->iterm / 128 - 1; // manual section 9.5.1.4, table 11 + ret = bq24190_write_mask(bdi, BQ24190_REG_PCTCC, + BQ24190_REG_PCTCC_ITERM_MASK, + BQ24190_REG_PCTCC_ITERM_SHIFT, + v); + if (ret < 0) + return ret; + } + + return 0; +} + +static int bq24190_register_reset(struct bq24190_dev_info *bdi) +{ + int ret, limit = 100; + u8 v; + + /* + * This prop. can be passed on device instantiation from platform code: + * struct property_entry pe[] = + * { PROPERTY_ENTRY_BOOL("disable-reset"), ... }; + * struct i2c_board_info bi = + * { .type = "bq24190", .addr = 0x6b, .properties = pe, .irq = irq }; + * struct i2c_adapter ad = { ... }; + * i2c_add_adapter(&ad); + * i2c_new_client_device(&ad, &bi); + */ + if (device_property_read_bool(bdi->dev, "disable-reset")) + return 0; + + /* Reset the registers */ + ret = bq24190_write_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_RESET_MASK, + BQ24190_REG_POC_RESET_SHIFT, + 0x1); + if (ret < 0) + return ret; + + /* Reset bit will be cleared by hardware so poll until it is */ + do { + ret = bq24190_read_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_RESET_MASK, + BQ24190_REG_POC_RESET_SHIFT, + &v); + if (ret < 0) + return ret; + + if (v == 0) + return 0; + + usleep_range(100, 200); + } while (--limit); + + return -EIO; +} + +/* Charger power supply property routines */ + +static int bq24190_charger_get_charge_type(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int type, ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_CHG_CONFIG_MASK, + BQ24190_REG_POC_CHG_CONFIG_SHIFT, + &v); + if (ret < 0) + return ret; + + /* If POC[CHG_CONFIG] (REG01[5:4]) == 0, charge is disabled */ + if (!v) { + type = POWER_SUPPLY_CHARGE_TYPE_NONE; + } else { + ret = bq24190_read_mask(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_FORCE_20PCT_MASK, + BQ24190_REG_CCC_FORCE_20PCT_SHIFT, + &v); + if (ret < 0) + return ret; + + type = (v) ? POWER_SUPPLY_CHARGE_TYPE_TRICKLE : + POWER_SUPPLY_CHARGE_TYPE_FAST; + } + + val->intval = type; + + return 0; +} + +static int bq24190_charger_set_charge_type(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + u8 chg_config, force_20pct, en_term; + int ret; + + /* + * According to the "Termination when REG02[0] = 1" section of + * the bq24190 manual, the trickle charge could be less than the + * termination current so it recommends turning off the termination + * function. + * + * Note: AFAICT from the datasheet, the user will have to manually + * turn off the charging when in 20% mode. If its not turned off, + * there could be battery damage. So, use this mode at your own risk. + */ + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_NONE: + chg_config = 0x0; + break; + case POWER_SUPPLY_CHARGE_TYPE_TRICKLE: + chg_config = 0x1; + force_20pct = 0x1; + en_term = 0x0; + break; + case POWER_SUPPLY_CHARGE_TYPE_FAST: + chg_config = 0x1; + force_20pct = 0x0; + en_term = 0x1; + break; + default: + return -EINVAL; + } + + if (chg_config) { /* Enabling the charger */ + ret = bq24190_write_mask(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_FORCE_20PCT_MASK, + BQ24190_REG_CCC_FORCE_20PCT_SHIFT, + force_20pct); + if (ret < 0) + return ret; + + ret = bq24190_write_mask(bdi, BQ24190_REG_CTTC, + BQ24190_REG_CTTC_EN_TERM_MASK, + BQ24190_REG_CTTC_EN_TERM_SHIFT, + en_term); + if (ret < 0) + return ret; + } + + return bq24190_write_mask(bdi, BQ24190_REG_POC, + BQ24190_REG_POC_CHG_CONFIG_MASK, + BQ24190_REG_POC_CHG_CONFIG_SHIFT, chg_config); +} + +static int bq24190_charger_get_health(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int health; + + mutex_lock(&bdi->f_reg_lock); + v = bdi->f_reg; + mutex_unlock(&bdi->f_reg_lock); + + if (v & BQ24190_REG_F_NTC_FAULT_MASK) { + switch (v >> BQ24190_REG_F_NTC_FAULT_SHIFT & 0x7) { + case 0x1: /* TS1 Cold */ + case 0x3: /* TS2 Cold */ + case 0x5: /* Both Cold */ + health = POWER_SUPPLY_HEALTH_COLD; + break; + case 0x2: /* TS1 Hot */ + case 0x4: /* TS2 Hot */ + case 0x6: /* Both Hot */ + health = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + default: + health = POWER_SUPPLY_HEALTH_UNKNOWN; + } + } else if (v & BQ24190_REG_F_BAT_FAULT_MASK) { + health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + } else if (v & BQ24190_REG_F_CHRG_FAULT_MASK) { + switch (v >> BQ24190_REG_F_CHRG_FAULT_SHIFT & 0x3) { + case 0x1: /* Input Fault (VBUS OVP or VBAT<VBUS<3.8V) */ + /* + * This could be over-voltage or under-voltage + * and there's no way to tell which. Instead + * of looking foolish and returning 'OVERVOLTAGE' + * when its really under-voltage, just return + * 'UNSPEC_FAILURE'. + */ + health = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + case 0x2: /* Thermal Shutdown */ + health = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + case 0x3: /* Charge Safety Timer Expiration */ + health = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + break; + default: /* prevent compiler warning */ + health = -1; + } + } else if (v & BQ24190_REG_F_BOOST_FAULT_MASK) { + /* + * This could be over-current or over-voltage but there's + * no way to tell which. Return 'OVERVOLTAGE' since there + * isn't an 'OVERCURRENT' value defined that we can return + * even if it was over-current. + */ + health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + } else { + health = POWER_SUPPLY_HEALTH_GOOD; + } + + val->intval = health; + + return 0; +} + +static int bq24190_charger_get_online(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 pg_stat, batfet_disable; + int ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_SS, + BQ24190_REG_SS_PG_STAT_MASK, + BQ24190_REG_SS_PG_STAT_SHIFT, &pg_stat); + if (ret < 0) + return ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_MOC, + BQ24190_REG_MOC_BATFET_DISABLE_MASK, + BQ24190_REG_MOC_BATFET_DISABLE_SHIFT, &batfet_disable); + if (ret < 0) + return ret; + + val->intval = pg_stat && !batfet_disable; + + return 0; +} + +static int bq24190_battery_set_online(struct bq24190_dev_info *bdi, + const union power_supply_propval *val); +static int bq24190_battery_get_status(struct bq24190_dev_info *bdi, + union power_supply_propval *val); +static int bq24190_battery_get_temp_alert_max(struct bq24190_dev_info *bdi, + union power_supply_propval *val); +static int bq24190_battery_set_temp_alert_max(struct bq24190_dev_info *bdi, + const union power_supply_propval *val); + +static int bq24190_charger_set_online(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_battery_set_online(bdi, val); +} + +static int bq24190_charger_get_status(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + return bq24190_battery_get_status(bdi, val); +} + +static int bq24190_charger_get_temp_alert_max(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + return bq24190_battery_get_temp_alert_max(bdi, val); +} + +static int bq24190_charger_set_temp_alert_max(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_battery_set_temp_alert_max(bdi, val); +} + +static int bq24190_charger_get_precharge(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_PCTCC, + BQ24190_REG_PCTCC_IPRECHG_MASK, + BQ24190_REG_PCTCC_IPRECHG_SHIFT, &v); + if (ret < 0) + return ret; + + val->intval = ++v * 128 * 1000; + return 0; +} + +static int bq24190_charger_get_charge_term(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_PCTCC, + BQ24190_REG_PCTCC_ITERM_MASK, + BQ24190_REG_PCTCC_ITERM_SHIFT, &v); + if (ret < 0) + return ret; + + val->intval = ++v * 128 * 1000; + return 0; +} + +static int bq24190_charger_get_current(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int curr, ret; + + ret = bq24190_get_field_val(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_ICHG_MASK, BQ24190_REG_CCC_ICHG_SHIFT, + bq24190_ccc_ichg_values, + ARRAY_SIZE(bq24190_ccc_ichg_values), &curr); + if (ret < 0) + return ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_FORCE_20PCT_MASK, + BQ24190_REG_CCC_FORCE_20PCT_SHIFT, &v); + if (ret < 0) + return ret; + + /* If FORCE_20PCT is enabled, then current is 20% of ICHG value */ + if (v) + curr /= 5; + + val->intval = curr; + return 0; +} + +static int bq24190_charger_get_current_max(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + int idx = ARRAY_SIZE(bq24190_ccc_ichg_values) - 1; + + val->intval = bq24190_ccc_ichg_values[idx]; + return 0; +} + +static int bq24190_charger_set_current(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + u8 v; + int ret, curr = val->intval; + + ret = bq24190_read_mask(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_FORCE_20PCT_MASK, + BQ24190_REG_CCC_FORCE_20PCT_SHIFT, &v); + if (ret < 0) + return ret; + + /* If FORCE_20PCT is enabled, have to multiply value passed in by 5 */ + if (v) + curr *= 5; + + return bq24190_set_field_val(bdi, BQ24190_REG_CCC, + BQ24190_REG_CCC_ICHG_MASK, BQ24190_REG_CCC_ICHG_SHIFT, + bq24190_ccc_ichg_values, + ARRAY_SIZE(bq24190_ccc_ichg_values), curr); +} + +static int bq24190_charger_get_voltage(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + int voltage, ret; + + ret = bq24190_get_field_val(bdi, BQ24190_REG_CVC, + BQ24190_REG_CVC_VREG_MASK, BQ24190_REG_CVC_VREG_SHIFT, + bq24190_cvc_vreg_values, + ARRAY_SIZE(bq24190_cvc_vreg_values), &voltage); + if (ret < 0) + return ret; + + val->intval = voltage; + return 0; +} + +static int bq24190_charger_get_voltage_max(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + int idx = ARRAY_SIZE(bq24190_cvc_vreg_values) - 1; + + val->intval = bq24190_cvc_vreg_values[idx]; + return 0; +} + +static int bq24190_charger_set_voltage(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_set_field_val(bdi, BQ24190_REG_CVC, + BQ24190_REG_CVC_VREG_MASK, BQ24190_REG_CVC_VREG_SHIFT, + bq24190_cvc_vreg_values, + ARRAY_SIZE(bq24190_cvc_vreg_values), val->intval); +} + +static int bq24190_charger_get_iinlimit(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + int iinlimit, ret; + + ret = bq24190_get_field_val(bdi, BQ24190_REG_ISC, + BQ24190_REG_ISC_IINLIM_MASK, + BQ24190_REG_ISC_IINLIM_SHIFT, + bq24190_isc_iinlim_values, + ARRAY_SIZE(bq24190_isc_iinlim_values), &iinlimit); + if (ret < 0) + return ret; + + val->intval = iinlimit; + return 0; +} + +static int bq24190_charger_set_iinlimit(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_set_field_val(bdi, BQ24190_REG_ISC, + BQ24190_REG_ISC_IINLIM_MASK, + BQ24190_REG_ISC_IINLIM_SHIFT, + bq24190_isc_iinlim_values, + ARRAY_SIZE(bq24190_isc_iinlim_values), val->intval); +} + +static int bq24190_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + int ret; + + dev_dbg(bdi->dev, "prop: %d\n", psp); + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = bq24190_charger_get_charge_type(bdi, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = bq24190_charger_get_health(bdi, val); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = bq24190_charger_get_online(bdi, val); + break; + case POWER_SUPPLY_PROP_STATUS: + ret = bq24190_charger_get_status(bdi, val); + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = bq24190_charger_get_temp_alert_max(bdi, val); + break; + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + ret = bq24190_charger_get_precharge(bdi, val); + break; + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + ret = bq24190_charger_get_charge_term(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq24190_charger_get_current(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + ret = bq24190_charger_get_current_max(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq24190_charger_get_voltage(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + ret = bq24190_charger_get_voltage_max(bdi, val); + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq24190_charger_get_iinlimit(bdi, val); + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + ret = 0; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bdi->model_name; + ret = 0; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ24190_MANUFACTURER; + ret = 0; + break; + default: + ret = -ENODATA; + } + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return ret; +} + +static int bq24190_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + int ret; + + dev_dbg(bdi->dev, "prop: %d\n", psp); + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = bq24190_charger_set_online(bdi, val); + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = bq24190_charger_set_temp_alert_max(bdi, val); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = bq24190_charger_set_charge_type(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq24190_charger_set_current(bdi, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq24190_charger_set_voltage(bdi, val); + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq24190_charger_set_iinlimit(bdi, val); + break; + default: + ret = -EINVAL; + } + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return ret; +} + +static int bq24190_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + case POWER_SUPPLY_PROP_CHARGE_TYPE: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return 1; + default: + return 0; + } +} + +static void bq24190_input_current_limit_work(struct work_struct *work) +{ + struct bq24190_dev_info *bdi = + container_of(work, struct bq24190_dev_info, + input_current_limit_work.work); + union power_supply_propval val; + int ret; + + ret = power_supply_get_property_from_supplier(bdi->charger, + POWER_SUPPLY_PROP_CURRENT_MAX, + &val); + if (ret) + return; + + bq24190_charger_set_property(bdi->charger, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + &val); + power_supply_changed(bdi->charger); +} + +/* Sync the input-current-limit with our parent supply (if we have one) */ +static void bq24190_charger_external_power_changed(struct power_supply *psy) +{ + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + + /* + * The Power-Good detection may take up to 220ms, sometimes + * the external charger detection is quicker, and the bq24190 will + * reset to iinlim based on its own charger detection (which is not + * hooked up when using external charger detection) resulting in a + * too low default 500mA iinlim. Delay setting the input-current-limit + * for 300ms to avoid this. + */ + queue_delayed_work(system_wq, &bdi->input_current_limit_work, + msecs_to_jiffies(300)); +} + +static enum power_supply_property bq24190_charger_properties[] = { + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TEMP_ALERT_MAX, + POWER_SUPPLY_PROP_PRECHARGE_CURRENT, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + 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, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static char *bq24190_charger_supplied_to[] = { + "main-battery", +}; + +static const struct power_supply_desc bq24190_charger_desc = { + .name = "bq24190-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = bq24190_charger_properties, + .num_properties = ARRAY_SIZE(bq24190_charger_properties), + .get_property = bq24190_charger_get_property, + .set_property = bq24190_charger_set_property, + .property_is_writeable = bq24190_charger_property_is_writeable, + .external_power_changed = bq24190_charger_external_power_changed, +}; + +/* Battery power supply property routines */ + +static int bq24190_battery_get_status(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 ss_reg, chrg_fault; + int status, ret; + + mutex_lock(&bdi->f_reg_lock); + chrg_fault = bdi->f_reg; + mutex_unlock(&bdi->f_reg_lock); + + chrg_fault &= BQ24190_REG_F_CHRG_FAULT_MASK; + chrg_fault >>= BQ24190_REG_F_CHRG_FAULT_SHIFT; + + ret = bq24190_read(bdi, BQ24190_REG_SS, &ss_reg); + if (ret < 0) + return ret; + + /* + * The battery must be discharging when any of these are true: + * - there is no good power source; + * - there is a charge fault. + * Could also be discharging when in "supplement mode" but + * there is no way to tell when its in that mode. + */ + if (!(ss_reg & BQ24190_REG_SS_PG_STAT_MASK) || chrg_fault) { + status = POWER_SUPPLY_STATUS_DISCHARGING; + } else { + ss_reg &= BQ24190_REG_SS_CHRG_STAT_MASK; + ss_reg >>= BQ24190_REG_SS_CHRG_STAT_SHIFT; + + switch (ss_reg) { + case 0x0: /* Not Charging */ + status = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case 0x1: /* Pre-charge */ + case 0x2: /* Fast Charging */ + status = POWER_SUPPLY_STATUS_CHARGING; + break; + case 0x3: /* Charge Termination Done */ + status = POWER_SUPPLY_STATUS_FULL; + break; + default: + ret = -EIO; + } + } + + if (!ret) + val->intval = status; + + return ret; +} + +static int bq24190_battery_get_health(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 v; + int health; + + mutex_lock(&bdi->f_reg_lock); + v = bdi->f_reg; + mutex_unlock(&bdi->f_reg_lock); + + if (v & BQ24190_REG_F_BAT_FAULT_MASK) { + health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + } else { + v &= BQ24190_REG_F_NTC_FAULT_MASK; + v >>= BQ24190_REG_F_NTC_FAULT_SHIFT; + + switch (v) { + case 0x0: /* Normal */ + health = POWER_SUPPLY_HEALTH_GOOD; + break; + case 0x1: /* TS1 Cold */ + case 0x3: /* TS2 Cold */ + case 0x5: /* Both Cold */ + health = POWER_SUPPLY_HEALTH_COLD; + break; + case 0x2: /* TS1 Hot */ + case 0x4: /* TS2 Hot */ + case 0x6: /* Both Hot */ + health = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + default: + health = POWER_SUPPLY_HEALTH_UNKNOWN; + } + } + + val->intval = health; + return 0; +} + +static int bq24190_battery_get_online(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + u8 batfet_disable; + int ret; + + ret = bq24190_read_mask(bdi, BQ24190_REG_MOC, + BQ24190_REG_MOC_BATFET_DISABLE_MASK, + BQ24190_REG_MOC_BATFET_DISABLE_SHIFT, &batfet_disable); + if (ret < 0) + return ret; + + val->intval = !batfet_disable; + return 0; +} + +static int bq24190_battery_set_online(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_write_mask(bdi, BQ24190_REG_MOC, + BQ24190_REG_MOC_BATFET_DISABLE_MASK, + BQ24190_REG_MOC_BATFET_DISABLE_SHIFT, !val->intval); +} + +static int bq24190_battery_get_temp_alert_max(struct bq24190_dev_info *bdi, + union power_supply_propval *val) +{ + int temp, ret; + + ret = bq24190_get_field_val(bdi, BQ24190_REG_ICTRC, + BQ24190_REG_ICTRC_TREG_MASK, + BQ24190_REG_ICTRC_TREG_SHIFT, + bq24190_ictrc_treg_values, + ARRAY_SIZE(bq24190_ictrc_treg_values), &temp); + if (ret < 0) + return ret; + + val->intval = temp; + return 0; +} + +static int bq24190_battery_set_temp_alert_max(struct bq24190_dev_info *bdi, + const union power_supply_propval *val) +{ + return bq24190_set_field_val(bdi, BQ24190_REG_ICTRC, + BQ24190_REG_ICTRC_TREG_MASK, + BQ24190_REG_ICTRC_TREG_SHIFT, + bq24190_ictrc_treg_values, + ARRAY_SIZE(bq24190_ictrc_treg_values), val->intval); +} + +static int bq24190_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + int ret; + + dev_warn(bdi->dev, "warning: /sys/class/power_supply/bq24190-battery is deprecated\n"); + dev_dbg(bdi->dev, "prop: %d\n", psp); + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = bq24190_battery_get_status(bdi, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = bq24190_battery_get_health(bdi, val); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = bq24190_battery_get_online(bdi, val); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + /* Could be Li-on or Li-polymer but no way to tell which */ + val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + ret = 0; + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = bq24190_battery_get_temp_alert_max(bdi, val); + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + ret = 0; + break; + default: + ret = -ENODATA; + } + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return ret; +} + +static int bq24190_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct bq24190_dev_info *bdi = power_supply_get_drvdata(psy); + int ret; + + dev_warn(bdi->dev, "warning: /sys/class/power_supply/bq24190-battery is deprecated\n"); + dev_dbg(bdi->dev, "prop: %d\n", psp); + + ret = pm_runtime_resume_and_get(bdi->dev); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = bq24190_battery_set_online(bdi, val); + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = bq24190_battery_set_temp_alert_max(bdi, val); + break; + default: + ret = -EINVAL; + } + + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + + return ret; +} + +static int bq24190_battery_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} + +static enum power_supply_property bq24190_battery_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_TEMP_ALERT_MAX, + POWER_SUPPLY_PROP_SCOPE, +}; + +static const struct power_supply_desc bq24190_battery_desc = { + .name = "bq24190-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = bq24190_battery_properties, + .num_properties = ARRAY_SIZE(bq24190_battery_properties), + .get_property = bq24190_battery_get_property, + .set_property = bq24190_battery_set_property, + .property_is_writeable = bq24190_battery_property_is_writeable, +}; + +static int bq24190_configure_usb_otg(struct bq24190_dev_info *bdi, u8 ss_reg) +{ + bool otg_enabled; + int ret; + + otg_enabled = !!(ss_reg & BQ24190_REG_SS_VBUS_STAT_MASK); + ret = extcon_set_state_sync(bdi->edev, EXTCON_USB, otg_enabled); + if (ret < 0) + dev_err(bdi->dev, "Can't set extcon state to %d: %d\n", + otg_enabled, ret); + + return ret; +} + +static void bq24190_check_status(struct bq24190_dev_info *bdi) +{ + const u8 battery_mask_ss = BQ24190_REG_SS_CHRG_STAT_MASK; + const u8 battery_mask_f = BQ24190_REG_F_BAT_FAULT_MASK + | BQ24190_REG_F_NTC_FAULT_MASK; + bool alert_charger = false, alert_battery = false; + u8 ss_reg = 0, f_reg = 0; + int i, ret; + + ret = bq24190_read(bdi, BQ24190_REG_SS, &ss_reg); + if (ret < 0) { + dev_err(bdi->dev, "Can't read SS reg: %d\n", ret); + return; + } + + i = 0; + do { + ret = bq24190_read(bdi, BQ24190_REG_F, &f_reg); + if (ret < 0) { + dev_err(bdi->dev, "Can't read F reg: %d\n", ret); + return; + } + } while (f_reg && ++i < 2); + + /* ignore over/under voltage fault after disconnect */ + if (f_reg == (1 << BQ24190_REG_F_CHRG_FAULT_SHIFT) && + !(ss_reg & BQ24190_REG_SS_PG_STAT_MASK)) + f_reg = 0; + + if (f_reg != bdi->f_reg) { + dev_warn(bdi->dev, + "Fault: boost %d, charge %d, battery %d, ntc %d\n", + !!(f_reg & BQ24190_REG_F_BOOST_FAULT_MASK), + !!(f_reg & BQ24190_REG_F_CHRG_FAULT_MASK), + !!(f_reg & BQ24190_REG_F_BAT_FAULT_MASK), + !!(f_reg & BQ24190_REG_F_NTC_FAULT_MASK)); + + mutex_lock(&bdi->f_reg_lock); + if ((bdi->f_reg & battery_mask_f) != (f_reg & battery_mask_f)) + alert_battery = true; + if ((bdi->f_reg & ~battery_mask_f) != (f_reg & ~battery_mask_f)) + alert_charger = true; + bdi->f_reg = f_reg; + mutex_unlock(&bdi->f_reg_lock); + } + + if (ss_reg != bdi->ss_reg) { + /* + * The device is in host mode so when PG_STAT goes from 1->0 + * (i.e., power removed) HIZ needs to be disabled. + */ + if ((bdi->ss_reg & BQ24190_REG_SS_PG_STAT_MASK) && + !(ss_reg & BQ24190_REG_SS_PG_STAT_MASK)) { + ret = bq24190_write_mask(bdi, BQ24190_REG_ISC, + BQ24190_REG_ISC_EN_HIZ_MASK, + BQ24190_REG_ISC_EN_HIZ_SHIFT, + 0); + if (ret < 0) + dev_err(bdi->dev, "Can't access ISC reg: %d\n", + ret); + } + + if ((bdi->ss_reg & battery_mask_ss) != (ss_reg & battery_mask_ss)) + alert_battery = true; + if ((bdi->ss_reg & ~battery_mask_ss) != (ss_reg & ~battery_mask_ss)) + alert_charger = true; + bdi->ss_reg = ss_reg; + } + + if (alert_charger || alert_battery) { + power_supply_changed(bdi->charger); + bq24190_configure_usb_otg(bdi, ss_reg); + } + if (alert_battery && bdi->battery) + power_supply_changed(bdi->battery); + + dev_dbg(bdi->dev, "ss_reg: 0x%02x, f_reg: 0x%02x\n", ss_reg, f_reg); +} + +static irqreturn_t bq24190_irq_handler_thread(int irq, void *data) +{ + struct bq24190_dev_info *bdi = data; + int error; + + bdi->irq_event = true; + error = pm_runtime_resume_and_get(bdi->dev); + if (error < 0) { + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", error); + return IRQ_NONE; + } + bq24190_check_status(bdi); + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + bdi->irq_event = false; + + return IRQ_HANDLED; +} + +static int bq24190_hw_init(struct bq24190_dev_info *bdi) +{ + u8 v; + int ret; + + /* First check that the device really is what its supposed to be */ + ret = bq24190_read_mask(bdi, BQ24190_REG_VPRS, + BQ24190_REG_VPRS_PN_MASK, + BQ24190_REG_VPRS_PN_SHIFT, + &v); + if (ret < 0) + return ret; + + switch (v) { + case BQ24190_REG_VPRS_PN_24190: + case BQ24190_REG_VPRS_PN_24192: + case BQ24190_REG_VPRS_PN_24192I: + break; + default: + dev_err(bdi->dev, "Error unknown model: 0x%02x\n", v); + return -ENODEV; + } + + ret = bq24190_register_reset(bdi); + if (ret < 0) + return ret; + + ret = bq24190_set_config(bdi); + if (ret < 0) + return ret; + + return bq24190_read(bdi, BQ24190_REG_SS, &bdi->ss_reg); +} + +static int bq24190_get_config(struct bq24190_dev_info *bdi) +{ + const char * const s = "ti,system-minimum-microvolt"; + struct power_supply_battery_info info = {}; + int v; + + if (device_property_read_u32(bdi->dev, s, &v) == 0) { + v /= 1000; + if (v >= BQ24190_REG_POC_SYS_MIN_MIN + && v <= BQ24190_REG_POC_SYS_MIN_MAX) + bdi->sys_min = v; + else + dev_warn(bdi->dev, "invalid value for %s: %u\n", s, v); + } + + if (bdi->dev->of_node && + !power_supply_get_battery_info(bdi->charger, &info)) { + v = info.precharge_current_ua / 1000; + if (v >= BQ24190_REG_PCTCC_IPRECHG_MIN + && v <= BQ24190_REG_PCTCC_IPRECHG_MAX) + bdi->iprechg = v; + else + dev_warn(bdi->dev, "invalid value for battery:precharge-current-microamp: %d\n", + v); + + v = info.charge_term_current_ua / 1000; + if (v >= BQ24190_REG_PCTCC_ITERM_MIN + && v <= BQ24190_REG_PCTCC_ITERM_MAX) + bdi->iterm = v; + else + dev_warn(bdi->dev, "invalid value for battery:charge-term-current-microamp: %d\n", + v); + } + + return 0; +} + +static int bq24190_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct device *dev = &client->dev; + struct power_supply_config charger_cfg = {}, battery_cfg = {}; + struct bq24190_dev_info *bdi; + int ret; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { + dev_err(dev, "No support for SMBUS_BYTE_DATA\n"); + return -ENODEV; + } + + bdi = devm_kzalloc(dev, sizeof(*bdi), GFP_KERNEL); + if (!bdi) { + dev_err(dev, "Can't alloc bdi struct\n"); + return -ENOMEM; + } + + bdi->client = client; + bdi->dev = dev; + strncpy(bdi->model_name, id->name, I2C_NAME_SIZE); + mutex_init(&bdi->f_reg_lock); + bdi->f_reg = 0; + bdi->ss_reg = BQ24190_REG_SS_VBUS_STAT_MASK; /* impossible state */ + INIT_DELAYED_WORK(&bdi->input_current_limit_work, + bq24190_input_current_limit_work); + + i2c_set_clientdata(client, bdi); + + if (client->irq <= 0) { + dev_err(dev, "Can't get irq info\n"); + return -EINVAL; + } + + bdi->edev = devm_extcon_dev_allocate(dev, bq24190_usb_extcon_cable); + if (IS_ERR(bdi->edev)) + return PTR_ERR(bdi->edev); + + ret = devm_extcon_dev_register(dev, bdi->edev); + if (ret < 0) + return ret; + + pm_runtime_enable(dev); + pm_runtime_use_autosuspend(dev); + pm_runtime_set_autosuspend_delay(dev, 600); + ret = pm_runtime_get_sync(dev); + if (ret < 0) { + dev_err(dev, "pm_runtime_get failed: %i\n", ret); + goto out_pmrt; + } + +#ifdef CONFIG_SYSFS + bq24190_sysfs_init_attrs(); + charger_cfg.attr_grp = bq24190_sysfs_groups; +#endif + + charger_cfg.drv_data = bdi; + charger_cfg.of_node = dev->of_node; + charger_cfg.supplied_to = bq24190_charger_supplied_to; + charger_cfg.num_supplicants = ARRAY_SIZE(bq24190_charger_supplied_to), + bdi->charger = power_supply_register(dev, &bq24190_charger_desc, + &charger_cfg); + if (IS_ERR(bdi->charger)) { + dev_err(dev, "Can't register charger\n"); + ret = PTR_ERR(bdi->charger); + goto out_pmrt; + } + + /* the battery class is deprecated and will be removed. */ + /* in the interim, this property hides it. */ + if (!device_property_read_bool(dev, "omit-battery-class")) { + battery_cfg.drv_data = bdi; + bdi->battery = power_supply_register(dev, &bq24190_battery_desc, + &battery_cfg); + if (IS_ERR(bdi->battery)) { + dev_err(dev, "Can't register battery\n"); + ret = PTR_ERR(bdi->battery); + goto out_charger; + } + } + + ret = bq24190_get_config(bdi); + if (ret < 0) { + dev_err(dev, "Can't get devicetree config\n"); + goto out_charger; + } + + ret = bq24190_hw_init(bdi); + if (ret < 0) { + dev_err(dev, "Hardware init failed\n"); + goto out_charger; + } + + ret = bq24190_configure_usb_otg(bdi, bdi->ss_reg); + if (ret < 0) + goto out_charger; + + bdi->initialized = true; + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + bq24190_irq_handler_thread, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + "bq24190-charger", bdi); + if (ret < 0) { + dev_err(dev, "Can't set up irq handler\n"); + goto out_charger; + } + + ret = bq24190_register_vbus_regulator(bdi); + if (ret < 0) + goto out_charger; + + enable_irq_wake(client->irq); + + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + + return 0; + +out_charger: + if (!IS_ERR_OR_NULL(bdi->battery)) + power_supply_unregister(bdi->battery); + power_supply_unregister(bdi->charger); + +out_pmrt: + pm_runtime_put_sync(dev); + pm_runtime_dont_use_autosuspend(dev); + pm_runtime_disable(dev); + return ret; +} + +static int bq24190_remove(struct i2c_client *client) +{ + struct bq24190_dev_info *bdi = i2c_get_clientdata(client); + int error; + + cancel_delayed_work_sync(&bdi->input_current_limit_work); + error = pm_runtime_resume_and_get(bdi->dev); + if (error < 0) + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", error); + + bq24190_register_reset(bdi); + if (bdi->battery) + power_supply_unregister(bdi->battery); + power_supply_unregister(bdi->charger); + if (error >= 0) + pm_runtime_put_sync(bdi->dev); + pm_runtime_dont_use_autosuspend(bdi->dev); + pm_runtime_disable(bdi->dev); + + return 0; +} + +static __maybe_unused int bq24190_runtime_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bq24190_dev_info *bdi = i2c_get_clientdata(client); + + if (!bdi->initialized) + return 0; + + dev_dbg(bdi->dev, "%s\n", __func__); + + return 0; +} + +static __maybe_unused int bq24190_runtime_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bq24190_dev_info *bdi = i2c_get_clientdata(client); + + if (!bdi->initialized) + return 0; + + if (!bdi->irq_event) { + dev_dbg(bdi->dev, "checking events on possible wakeirq\n"); + bq24190_check_status(bdi); + } + + return 0; +} + +static __maybe_unused int bq24190_pm_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bq24190_dev_info *bdi = i2c_get_clientdata(client); + int error; + + error = pm_runtime_resume_and_get(bdi->dev); + if (error < 0) + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", error); + + bq24190_register_reset(bdi); + + if (error >= 0) { + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + } + + return 0; +} + +static __maybe_unused int bq24190_pm_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bq24190_dev_info *bdi = i2c_get_clientdata(client); + int error; + + bdi->f_reg = 0; + bdi->ss_reg = BQ24190_REG_SS_VBUS_STAT_MASK; /* impossible state */ + + error = pm_runtime_resume_and_get(bdi->dev); + if (error < 0) + dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", error); + + bq24190_register_reset(bdi); + bq24190_set_config(bdi); + bq24190_read(bdi, BQ24190_REG_SS, &bdi->ss_reg); + + if (error >= 0) { + pm_runtime_mark_last_busy(bdi->dev); + pm_runtime_put_autosuspend(bdi->dev); + } + + /* Things may have changed while suspended so alert upper layer */ + power_supply_changed(bdi->charger); + if (bdi->battery) + power_supply_changed(bdi->battery); + + return 0; +} + +static const struct dev_pm_ops bq24190_pm_ops = { + SET_RUNTIME_PM_OPS(bq24190_runtime_suspend, bq24190_runtime_resume, + NULL) + SET_SYSTEM_SLEEP_PM_OPS(bq24190_pm_suspend, bq24190_pm_resume) +}; + +static const struct i2c_device_id bq24190_i2c_ids[] = { + { "bq24190" }, + { "bq24192" }, + { "bq24192i" }, + { "bq24196" }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, bq24190_i2c_ids); + +#ifdef CONFIG_OF +static const struct of_device_id bq24190_of_match[] = { + { .compatible = "ti,bq24190", }, + { .compatible = "ti,bq24192", }, + { .compatible = "ti,bq24192i", }, + { .compatible = "ti,bq24196", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bq24190_of_match); +#else +static const struct of_device_id bq24190_of_match[] = { + { }, +}; +#endif + +static struct i2c_driver bq24190_driver = { + .probe = bq24190_probe, + .remove = bq24190_remove, + .id_table = bq24190_i2c_ids, + .driver = { + .name = "bq24190-charger", + .pm = &bq24190_pm_ops, + .of_match_table = of_match_ptr(bq24190_of_match), + }, +}; +module_i2c_driver(bq24190_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Mark A. Greer <mgreer@animalcreek.com>"); +MODULE_DESCRIPTION("TI BQ24190 Charger Driver"); diff --git a/drivers/power/supply/bq24257_charger.c b/drivers/power/supply/bq24257_charger.c new file mode 100644 index 000000000..96cb3290b --- /dev/null +++ b/drivers/power/supply/bq24257_charger.c @@ -0,0 +1,1180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * TI BQ24257 charger driver + * + * Copyright (C) 2015 Intel Corporation + * + * Datasheets: + * https://www.ti.com/product/bq24250 + * https://www.ti.com/product/bq24251 + * https://www.ti.com/product/bq24257 + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/types.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/delay.h> + +#include <linux/acpi.h> +#include <linux/of.h> + +#define BQ24257_REG_1 0x00 +#define BQ24257_REG_2 0x01 +#define BQ24257_REG_3 0x02 +#define BQ24257_REG_4 0x03 +#define BQ24257_REG_5 0x04 +#define BQ24257_REG_6 0x05 +#define BQ24257_REG_7 0x06 + +#define BQ24257_MANUFACTURER "Texas Instruments" +#define BQ24257_PG_GPIO "pg" + +#define BQ24257_ILIM_SET_DELAY 1000 /* msec */ + +/* + * When adding support for new devices make sure that enum bq2425x_chip and + * bq2425x_chip_name[] always stay in sync! + */ +enum bq2425x_chip { + BQ24250, + BQ24251, + BQ24257, +}; + +static const char *const bq2425x_chip_name[] = { + "bq24250", + "bq24251", + "bq24257", +}; + +enum bq24257_fields { + F_WD_FAULT, F_WD_EN, F_STAT, F_FAULT, /* REG 1 */ + F_RESET, F_IILIMIT, F_EN_STAT, F_EN_TERM, F_CE, F_HZ_MODE, /* REG 2 */ + F_VBAT, F_USB_DET, /* REG 3 */ + F_ICHG, F_ITERM, /* REG 4 */ + F_LOOP_STATUS, F_LOW_CHG, F_DPDM_EN, F_CE_STATUS, F_VINDPM, /* REG 5 */ + F_X2_TMR_EN, F_TMR, F_SYSOFF, F_TS_EN, F_TS_STAT, /* REG 6 */ + F_VOVP, F_CLR_VDP, F_FORCE_BATDET, F_FORCE_PTM, /* REG 7 */ + + F_MAX_FIELDS +}; + +/* initial field values, converted from uV/uA */ +struct bq24257_init_data { + u8 ichg; /* charge current */ + u8 vbat; /* regulation voltage */ + u8 iterm; /* termination current */ + u8 iilimit; /* input current limit */ + u8 vovp; /* over voltage protection voltage */ + u8 vindpm; /* VDMP input threshold voltage */ +}; + +struct bq24257_state { + u8 status; + u8 fault; + bool power_good; +}; + +struct bq24257_device { + struct i2c_client *client; + struct device *dev; + struct power_supply *charger; + + enum bq2425x_chip chip; + + struct regmap *rmap; + struct regmap_field *rmap_fields[F_MAX_FIELDS]; + + struct gpio_desc *pg; + + struct delayed_work iilimit_setup_work; + + struct bq24257_init_data init_data; + struct bq24257_state state; + + struct mutex lock; /* protect state data */ + + bool iilimit_autoset_enable; +}; + +static bool bq24257_is_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case BQ24257_REG_2: + case BQ24257_REG_4: + return false; + + default: + return true; + } +} + +static const struct regmap_config bq24257_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ24257_REG_7, + .cache_type = REGCACHE_RBTREE, + + .volatile_reg = bq24257_is_volatile_reg, +}; + +static const struct reg_field bq24257_reg_fields[] = { + /* REG 1 */ + [F_WD_FAULT] = REG_FIELD(BQ24257_REG_1, 7, 7), + [F_WD_EN] = REG_FIELD(BQ24257_REG_1, 6, 6), + [F_STAT] = REG_FIELD(BQ24257_REG_1, 4, 5), + [F_FAULT] = REG_FIELD(BQ24257_REG_1, 0, 3), + /* REG 2 */ + [F_RESET] = REG_FIELD(BQ24257_REG_2, 7, 7), + [F_IILIMIT] = REG_FIELD(BQ24257_REG_2, 4, 6), + [F_EN_STAT] = REG_FIELD(BQ24257_REG_2, 3, 3), + [F_EN_TERM] = REG_FIELD(BQ24257_REG_2, 2, 2), + [F_CE] = REG_FIELD(BQ24257_REG_2, 1, 1), + [F_HZ_MODE] = REG_FIELD(BQ24257_REG_2, 0, 0), + /* REG 3 */ + [F_VBAT] = REG_FIELD(BQ24257_REG_3, 2, 7), + [F_USB_DET] = REG_FIELD(BQ24257_REG_3, 0, 1), + /* REG 4 */ + [F_ICHG] = REG_FIELD(BQ24257_REG_4, 3, 7), + [F_ITERM] = REG_FIELD(BQ24257_REG_4, 0, 2), + /* REG 5 */ + [F_LOOP_STATUS] = REG_FIELD(BQ24257_REG_5, 6, 7), + [F_LOW_CHG] = REG_FIELD(BQ24257_REG_5, 5, 5), + [F_DPDM_EN] = REG_FIELD(BQ24257_REG_5, 4, 4), + [F_CE_STATUS] = REG_FIELD(BQ24257_REG_5, 3, 3), + [F_VINDPM] = REG_FIELD(BQ24257_REG_5, 0, 2), + /* REG 6 */ + [F_X2_TMR_EN] = REG_FIELD(BQ24257_REG_6, 7, 7), + [F_TMR] = REG_FIELD(BQ24257_REG_6, 5, 6), + [F_SYSOFF] = REG_FIELD(BQ24257_REG_6, 4, 4), + [F_TS_EN] = REG_FIELD(BQ24257_REG_6, 3, 3), + [F_TS_STAT] = REG_FIELD(BQ24257_REG_6, 0, 2), + /* REG 7 */ + [F_VOVP] = REG_FIELD(BQ24257_REG_7, 5, 7), + [F_CLR_VDP] = REG_FIELD(BQ24257_REG_7, 4, 4), + [F_FORCE_BATDET] = REG_FIELD(BQ24257_REG_7, 3, 3), + [F_FORCE_PTM] = REG_FIELD(BQ24257_REG_7, 2, 2) +}; + +static const u32 bq24257_vbat_map[] = { + 3500000, 3520000, 3540000, 3560000, 3580000, 3600000, 3620000, 3640000, + 3660000, 3680000, 3700000, 3720000, 3740000, 3760000, 3780000, 3800000, + 3820000, 3840000, 3860000, 3880000, 3900000, 3920000, 3940000, 3960000, + 3980000, 4000000, 4020000, 4040000, 4060000, 4080000, 4100000, 4120000, + 4140000, 4160000, 4180000, 4200000, 4220000, 4240000, 4260000, 4280000, + 4300000, 4320000, 4340000, 4360000, 4380000, 4400000, 4420000, 4440000 +}; + +#define BQ24257_VBAT_MAP_SIZE ARRAY_SIZE(bq24257_vbat_map) + +static const u32 bq24257_ichg_map[] = { + 500000, 550000, 600000, 650000, 700000, 750000, 800000, 850000, 900000, + 950000, 1000000, 1050000, 1100000, 1150000, 1200000, 1250000, 1300000, + 1350000, 1400000, 1450000, 1500000, 1550000, 1600000, 1650000, 1700000, + 1750000, 1800000, 1850000, 1900000, 1950000, 2000000 +}; + +#define BQ24257_ICHG_MAP_SIZE ARRAY_SIZE(bq24257_ichg_map) + +static const u32 bq24257_iterm_map[] = { + 50000, 75000, 100000, 125000, 150000, 175000, 200000, 225000 +}; + +#define BQ24257_ITERM_MAP_SIZE ARRAY_SIZE(bq24257_iterm_map) + +static const u32 bq24257_iilimit_map[] = { + 100000, 150000, 500000, 900000, 1500000, 2000000 +}; + +#define BQ24257_IILIMIT_MAP_SIZE ARRAY_SIZE(bq24257_iilimit_map) + +static const u32 bq24257_vovp_map[] = { + 6000000, 6500000, 7000000, 8000000, 9000000, 9500000, 10000000, + 10500000 +}; + +#define BQ24257_VOVP_MAP_SIZE ARRAY_SIZE(bq24257_vovp_map) + +static const u32 bq24257_vindpm_map[] = { + 4200000, 4280000, 4360000, 4440000, 4520000, 4600000, 4680000, + 4760000 +}; + +#define BQ24257_VINDPM_MAP_SIZE ARRAY_SIZE(bq24257_vindpm_map) + +static int bq24257_field_read(struct bq24257_device *bq, + enum bq24257_fields field_id) +{ + int ret; + int val; + + ret = regmap_field_read(bq->rmap_fields[field_id], &val); + if (ret < 0) + return ret; + + return val; +} + +static int bq24257_field_write(struct bq24257_device *bq, + enum bq24257_fields field_id, u8 val) +{ + return regmap_field_write(bq->rmap_fields[field_id], val); +} + +static u8 bq24257_find_idx(u32 value, const u32 *map, u8 map_size) +{ + u8 idx; + + for (idx = 1; idx < map_size; idx++) + if (value < map[idx]) + break; + + return idx - 1; +} + +enum bq24257_status { + STATUS_READY, + STATUS_CHARGE_IN_PROGRESS, + STATUS_CHARGE_DONE, + STATUS_FAULT, +}; + +enum bq24257_fault { + FAULT_NORMAL, + FAULT_INPUT_OVP, + FAULT_INPUT_UVLO, + FAULT_SLEEP, + FAULT_BAT_TS, + FAULT_BAT_OVP, + FAULT_TS, + FAULT_TIMER, + FAULT_NO_BAT, + FAULT_ISET, + FAULT_INPUT_LDO_LOW, +}; + +static int bq24257_get_input_current_limit(struct bq24257_device *bq, + union power_supply_propval *val) +{ + int ret; + + ret = bq24257_field_read(bq, F_IILIMIT); + if (ret < 0) + return ret; + + /* + * The "External ILIM" and "Production & Test" modes are not exposed + * through this driver and not being covered by the lookup table. + * Should such a mode have become active let's return an error rather + * than exceeding the bounds of the lookup table and returning + * garbage. + */ + if (ret >= BQ24257_IILIMIT_MAP_SIZE) + return -ENODATA; + + val->intval = bq24257_iilimit_map[ret]; + + return 0; +} + +static int bq24257_set_input_current_limit(struct bq24257_device *bq, + const union power_supply_propval *val) +{ + /* + * Address the case where the user manually sets an input current limit + * while the charger auto-detection mechanism is is active. In this + * case we want to abort and go straight to the user-specified value. + */ + if (bq->iilimit_autoset_enable) + cancel_delayed_work_sync(&bq->iilimit_setup_work); + + return bq24257_field_write(bq, F_IILIMIT, + bq24257_find_idx(val->intval, + bq24257_iilimit_map, + BQ24257_IILIMIT_MAP_SIZE)); +} + +static int bq24257_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq24257_device *bq = power_supply_get_drvdata(psy); + struct bq24257_state state; + + mutex_lock(&bq->lock); + state = bq->state; + mutex_unlock(&bq->lock); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (!state.power_good) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (state.status == STATUS_READY) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (state.status == STATUS_CHARGE_IN_PROGRESS) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (state.status == STATUS_CHARGE_DONE) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ24257_MANUFACTURER; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bq2425x_chip_name[bq->chip]; + break; + + case POWER_SUPPLY_PROP_ONLINE: + val->intval = state.power_good; + break; + + case POWER_SUPPLY_PROP_HEALTH: + switch (state.fault) { + case FAULT_NORMAL: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + + case FAULT_INPUT_OVP: + case FAULT_BAT_OVP: + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + break; + + case FAULT_TS: + case FAULT_BAT_TS: + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + + case FAULT_TIMER: + val->intval = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + break; + + default: + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + } + + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + val->intval = bq24257_ichg_map[bq->init_data.ichg]; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = bq24257_ichg_map[BQ24257_ICHG_MAP_SIZE - 1]; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + val->intval = bq24257_vbat_map[bq->init_data.vbat]; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + val->intval = bq24257_vbat_map[BQ24257_VBAT_MAP_SIZE - 1]; + break; + + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + val->intval = bq24257_iterm_map[bq->init_data.iterm]; + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return bq24257_get_input_current_limit(bq, val); + + default: + return -EINVAL; + } + + return 0; +} + +static int bq24257_power_supply_set_property(struct power_supply *psy, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct bq24257_device *bq = power_supply_get_drvdata(psy); + + switch (prop) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return bq24257_set_input_current_limit(bq, val); + default: + return -EINVAL; + } +} + +static int bq24257_power_supply_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return true; + default: + return false; + } +} + +static int bq24257_get_chip_state(struct bq24257_device *bq, + struct bq24257_state *state) +{ + int ret; + + ret = bq24257_field_read(bq, F_STAT); + if (ret < 0) + return ret; + + state->status = ret; + + ret = bq24257_field_read(bq, F_FAULT); + if (ret < 0) + return ret; + + state->fault = ret; + + if (bq->pg) + state->power_good = !gpiod_get_value_cansleep(bq->pg); + else + /* + * If we have a chip without a dedicated power-good GPIO or + * some other explicit bit that would provide this information + * assume the power is good if there is no supply related + * fault - and not good otherwise. There is a possibility for + * other errors to mask that power in fact is not good but this + * is probably the best we can do here. + */ + switch (state->fault) { + case FAULT_INPUT_OVP: + case FAULT_INPUT_UVLO: + case FAULT_INPUT_LDO_LOW: + state->power_good = false; + break; + default: + state->power_good = true; + } + + return 0; +} + +static bool bq24257_state_changed(struct bq24257_device *bq, + struct bq24257_state *new_state) +{ + int ret; + + mutex_lock(&bq->lock); + ret = (bq->state.status != new_state->status || + bq->state.fault != new_state->fault || + bq->state.power_good != new_state->power_good); + mutex_unlock(&bq->lock); + + return ret; +} + +enum bq24257_loop_status { + LOOP_STATUS_NONE, + LOOP_STATUS_IN_DPM, + LOOP_STATUS_IN_CURRENT_LIMIT, + LOOP_STATUS_THERMAL, +}; + +enum bq24257_in_ilimit { + IILIMIT_100, + IILIMIT_150, + IILIMIT_500, + IILIMIT_900, + IILIMIT_1500, + IILIMIT_2000, + IILIMIT_EXT, + IILIMIT_NONE, +}; + +enum bq24257_vovp { + VOVP_6000, + VOVP_6500, + VOVP_7000, + VOVP_8000, + VOVP_9000, + VOVP_9500, + VOVP_10000, + VOVP_10500 +}; + +enum bq24257_vindpm { + VINDPM_4200, + VINDPM_4280, + VINDPM_4360, + VINDPM_4440, + VINDPM_4520, + VINDPM_4600, + VINDPM_4680, + VINDPM_4760 +}; + +enum bq24257_port_type { + PORT_TYPE_DCP, /* Dedicated Charging Port */ + PORT_TYPE_CDP, /* Charging Downstream Port */ + PORT_TYPE_SDP, /* Standard Downstream Port */ + PORT_TYPE_NON_STANDARD, +}; + +enum bq24257_safety_timer { + SAFETY_TIMER_45, + SAFETY_TIMER_360, + SAFETY_TIMER_540, + SAFETY_TIMER_NONE, +}; + +static int bq24257_iilimit_autoset(struct bq24257_device *bq) +{ + int loop_status; + int iilimit; + int port_type; + int ret; + const u8 new_iilimit[] = { + [PORT_TYPE_DCP] = IILIMIT_2000, + [PORT_TYPE_CDP] = IILIMIT_2000, + [PORT_TYPE_SDP] = IILIMIT_500, + [PORT_TYPE_NON_STANDARD] = IILIMIT_500 + }; + + ret = bq24257_field_read(bq, F_LOOP_STATUS); + if (ret < 0) + goto error; + + loop_status = ret; + + ret = bq24257_field_read(bq, F_IILIMIT); + if (ret < 0) + goto error; + + iilimit = ret; + + /* + * All USB ports should be able to handle 500mA. If not, DPM will lower + * the charging current to accommodate the power source. No need to set + * a lower IILIMIT value. + */ + if (loop_status == LOOP_STATUS_IN_DPM && iilimit == IILIMIT_500) + return 0; + + ret = bq24257_field_read(bq, F_USB_DET); + if (ret < 0) + goto error; + + port_type = ret; + + ret = bq24257_field_write(bq, F_IILIMIT, new_iilimit[port_type]); + if (ret < 0) + goto error; + + ret = bq24257_field_write(bq, F_TMR, SAFETY_TIMER_360); + if (ret < 0) + goto error; + + ret = bq24257_field_write(bq, F_CLR_VDP, 1); + if (ret < 0) + goto error; + + dev_dbg(bq->dev, "port/loop = %d/%d -> iilimit = %d\n", + port_type, loop_status, new_iilimit[port_type]); + + return 0; + +error: + dev_err(bq->dev, "%s: Error communicating with the chip.\n", __func__); + return ret; +} + +static void bq24257_iilimit_setup_work(struct work_struct *work) +{ + struct bq24257_device *bq = container_of(work, struct bq24257_device, + iilimit_setup_work.work); + + bq24257_iilimit_autoset(bq); +} + +static void bq24257_handle_state_change(struct bq24257_device *bq, + struct bq24257_state *new_state) +{ + int ret; + struct bq24257_state old_state; + + mutex_lock(&bq->lock); + old_state = bq->state; + mutex_unlock(&bq->lock); + + /* + * Handle BQ2425x state changes observing whether the D+/D- based input + * current limit autoset functionality is enabled. + */ + if (!new_state->power_good) { + dev_dbg(bq->dev, "Power removed\n"); + if (bq->iilimit_autoset_enable) { + cancel_delayed_work_sync(&bq->iilimit_setup_work); + + /* activate D+/D- port detection algorithm */ + ret = bq24257_field_write(bq, F_DPDM_EN, 1); + if (ret < 0) + goto error; + } + /* + * When power is removed always return to the default input + * current limit as configured during probe. + */ + ret = bq24257_field_write(bq, F_IILIMIT, bq->init_data.iilimit); + if (ret < 0) + goto error; + } else if (!old_state.power_good) { + dev_dbg(bq->dev, "Power inserted\n"); + + if (bq->iilimit_autoset_enable) + /* configure input current limit */ + schedule_delayed_work(&bq->iilimit_setup_work, + msecs_to_jiffies(BQ24257_ILIM_SET_DELAY)); + } else if (new_state->fault == FAULT_NO_BAT) { + dev_warn(bq->dev, "Battery removed\n"); + } else if (new_state->fault == FAULT_TIMER) { + dev_err(bq->dev, "Safety timer expired! Battery dead?\n"); + } + + return; + +error: + dev_err(bq->dev, "%s: Error communicating with the chip.\n", __func__); +} + +static irqreturn_t bq24257_irq_handler_thread(int irq, void *private) +{ + int ret; + struct bq24257_device *bq = private; + struct bq24257_state state; + + ret = bq24257_get_chip_state(bq, &state); + if (ret < 0) + return IRQ_HANDLED; + + if (!bq24257_state_changed(bq, &state)) + return IRQ_HANDLED; + + dev_dbg(bq->dev, "irq(state changed): status/fault/pg = %d/%d/%d\n", + state.status, state.fault, state.power_good); + + bq24257_handle_state_change(bq, &state); + + mutex_lock(&bq->lock); + bq->state = state; + mutex_unlock(&bq->lock); + + power_supply_changed(bq->charger); + + return IRQ_HANDLED; +} + +static int bq24257_hw_init(struct bq24257_device *bq) +{ + int ret; + int i; + struct bq24257_state state; + + const struct { + int field; + u32 value; + } init_data[] = { + {F_ICHG, bq->init_data.ichg}, + {F_VBAT, bq->init_data.vbat}, + {F_ITERM, bq->init_data.iterm}, + {F_VOVP, bq->init_data.vovp}, + {F_VINDPM, bq->init_data.vindpm}, + }; + + /* + * Disable the watchdog timer to prevent the IC from going back to + * default settings after 50 seconds of I2C inactivity. + */ + ret = bq24257_field_write(bq, F_WD_EN, 0); + if (ret < 0) + return ret; + + /* configure the charge currents and voltages */ + for (i = 0; i < ARRAY_SIZE(init_data); i++) { + ret = bq24257_field_write(bq, init_data[i].field, + init_data[i].value); + if (ret < 0) + return ret; + } + + ret = bq24257_get_chip_state(bq, &state); + if (ret < 0) + return ret; + + mutex_lock(&bq->lock); + bq->state = state; + mutex_unlock(&bq->lock); + + if (!bq->iilimit_autoset_enable) { + dev_dbg(bq->dev, "manually setting iilimit = %u\n", + bq->init_data.iilimit); + + /* program fixed input current limit */ + ret = bq24257_field_write(bq, F_IILIMIT, + bq->init_data.iilimit); + if (ret < 0) + return ret; + } else if (!state.power_good) + /* activate D+/D- detection algorithm */ + ret = bq24257_field_write(bq, F_DPDM_EN, 1); + else if (state.fault != FAULT_NO_BAT) + ret = bq24257_iilimit_autoset(bq); + + return ret; +} + +static enum power_supply_property bq24257_power_supply_props[] = { + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, + 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, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, +}; + +static char *bq24257_charger_supplied_to[] = { + "main-battery", +}; + +static const struct power_supply_desc bq24257_power_supply_desc = { + .name = "bq24257-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = bq24257_power_supply_props, + .num_properties = ARRAY_SIZE(bq24257_power_supply_props), + .get_property = bq24257_power_supply_get_property, + .set_property = bq24257_power_supply_set_property, + .property_is_writeable = bq24257_power_supply_property_is_writeable, +}; + +static ssize_t bq24257_show_ovp_voltage(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24257_device *bq = power_supply_get_drvdata(psy); + + return scnprintf(buf, PAGE_SIZE, "%u\n", + bq24257_vovp_map[bq->init_data.vovp]); +} + +static ssize_t bq24257_show_in_dpm_voltage(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24257_device *bq = power_supply_get_drvdata(psy); + + return scnprintf(buf, PAGE_SIZE, "%u\n", + bq24257_vindpm_map[bq->init_data.vindpm]); +} + +static ssize_t bq24257_sysfs_show_enable(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24257_device *bq = power_supply_get_drvdata(psy); + int ret; + + if (strcmp(attr->attr.name, "high_impedance_enable") == 0) + ret = bq24257_field_read(bq, F_HZ_MODE); + else if (strcmp(attr->attr.name, "sysoff_enable") == 0) + ret = bq24257_field_read(bq, F_SYSOFF); + else + return -EINVAL; + + if (ret < 0) + return ret; + + return scnprintf(buf, PAGE_SIZE, "%d\n", ret); +} + +static ssize_t bq24257_sysfs_set_enable(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct bq24257_device *bq = power_supply_get_drvdata(psy); + long val; + int ret; + + if (kstrtol(buf, 10, &val) < 0) + return -EINVAL; + + if (strcmp(attr->attr.name, "high_impedance_enable") == 0) + ret = bq24257_field_write(bq, F_HZ_MODE, (bool)val); + else if (strcmp(attr->attr.name, "sysoff_enable") == 0) + ret = bq24257_field_write(bq, F_SYSOFF, (bool)val); + else + return -EINVAL; + + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(ovp_voltage, S_IRUGO, bq24257_show_ovp_voltage, NULL); +static DEVICE_ATTR(in_dpm_voltage, S_IRUGO, bq24257_show_in_dpm_voltage, NULL); +static DEVICE_ATTR(high_impedance_enable, S_IWUSR | S_IRUGO, + bq24257_sysfs_show_enable, bq24257_sysfs_set_enable); +static DEVICE_ATTR(sysoff_enable, S_IWUSR | S_IRUGO, + bq24257_sysfs_show_enable, bq24257_sysfs_set_enable); + +static struct attribute *bq24257_charger_sysfs_attrs[] = { + &dev_attr_ovp_voltage.attr, + &dev_attr_in_dpm_voltage.attr, + &dev_attr_high_impedance_enable.attr, + &dev_attr_sysoff_enable.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(bq24257_charger_sysfs); + +static int bq24257_power_supply_init(struct bq24257_device *bq) +{ + struct power_supply_config psy_cfg = { .drv_data = bq, }; + + psy_cfg.attr_grp = bq24257_charger_sysfs_groups; + psy_cfg.supplied_to = bq24257_charger_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(bq24257_charger_supplied_to); + + bq->charger = devm_power_supply_register(bq->dev, + &bq24257_power_supply_desc, + &psy_cfg); + + return PTR_ERR_OR_ZERO(bq->charger); +} + +static void bq24257_pg_gpio_probe(struct bq24257_device *bq) +{ + bq->pg = devm_gpiod_get_optional(bq->dev, BQ24257_PG_GPIO, GPIOD_IN); + + if (PTR_ERR(bq->pg) == -EPROBE_DEFER) { + dev_info(bq->dev, "probe retry requested for PG pin\n"); + return; + } else if (IS_ERR(bq->pg)) { + dev_err(bq->dev, "error probing PG pin\n"); + bq->pg = NULL; + return; + } + + if (bq->pg) + dev_dbg(bq->dev, "probed PG pin = %d\n", desc_to_gpio(bq->pg)); +} + +static int bq24257_fw_probe(struct bq24257_device *bq) +{ + int ret; + u32 property; + + /* Required properties */ + ret = device_property_read_u32(bq->dev, "ti,charge-current", &property); + if (ret < 0) + return ret; + + bq->init_data.ichg = bq24257_find_idx(property, bq24257_ichg_map, + BQ24257_ICHG_MAP_SIZE); + + ret = device_property_read_u32(bq->dev, "ti,battery-regulation-voltage", + &property); + if (ret < 0) + return ret; + + bq->init_data.vbat = bq24257_find_idx(property, bq24257_vbat_map, + BQ24257_VBAT_MAP_SIZE); + + ret = device_property_read_u32(bq->dev, "ti,termination-current", + &property); + if (ret < 0) + return ret; + + bq->init_data.iterm = bq24257_find_idx(property, bq24257_iterm_map, + BQ24257_ITERM_MAP_SIZE); + + /* Optional properties. If not provided use reasonable default. */ + ret = device_property_read_u32(bq->dev, "ti,current-limit", + &property); + if (ret < 0) { + bq->iilimit_autoset_enable = true; + + /* + * Explicitly set a default value which will be needed for + * devices that don't support the automatic setting of the input + * current limit through the charger type detection mechanism. + */ + bq->init_data.iilimit = IILIMIT_500; + } else + bq->init_data.iilimit = + bq24257_find_idx(property, + bq24257_iilimit_map, + BQ24257_IILIMIT_MAP_SIZE); + + ret = device_property_read_u32(bq->dev, "ti,ovp-voltage", + &property); + if (ret < 0) + bq->init_data.vovp = VOVP_6500; + else + bq->init_data.vovp = bq24257_find_idx(property, + bq24257_vovp_map, + BQ24257_VOVP_MAP_SIZE); + + ret = device_property_read_u32(bq->dev, "ti,in-dpm-voltage", + &property); + if (ret < 0) + bq->init_data.vindpm = VINDPM_4360; + else + bq->init_data.vindpm = + bq24257_find_idx(property, + bq24257_vindpm_map, + BQ24257_VINDPM_MAP_SIZE); + + return 0; +} + +static int bq24257_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct device *dev = &client->dev; + const struct acpi_device_id *acpi_id; + struct bq24257_device *bq; + int ret; + int i; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { + dev_err(dev, "No support for SMBUS_BYTE_DATA\n"); + return -ENODEV; + } + + bq = devm_kzalloc(dev, sizeof(*bq), GFP_KERNEL); + if (!bq) + return -ENOMEM; + + bq->client = client; + bq->dev = dev; + + if (ACPI_HANDLE(dev)) { + acpi_id = acpi_match_device(dev->driver->acpi_match_table, + &client->dev); + if (!acpi_id) { + dev_err(dev, "Failed to match ACPI device\n"); + return -ENODEV; + } + bq->chip = (enum bq2425x_chip)acpi_id->driver_data; + } else { + bq->chip = (enum bq2425x_chip)id->driver_data; + } + + mutex_init(&bq->lock); + + bq->rmap = devm_regmap_init_i2c(client, &bq24257_regmap_config); + if (IS_ERR(bq->rmap)) { + dev_err(dev, "failed to allocate register map\n"); + return PTR_ERR(bq->rmap); + } + + for (i = 0; i < ARRAY_SIZE(bq24257_reg_fields); i++) { + const struct reg_field *reg_fields = bq24257_reg_fields; + + bq->rmap_fields[i] = devm_regmap_field_alloc(dev, bq->rmap, + reg_fields[i]); + if (IS_ERR(bq->rmap_fields[i])) { + dev_err(dev, "cannot allocate regmap field\n"); + return PTR_ERR(bq->rmap_fields[i]); + } + } + + i2c_set_clientdata(client, bq); + + if (!dev->platform_data) { + ret = bq24257_fw_probe(bq); + if (ret < 0) { + dev_err(dev, "Cannot read device properties.\n"); + return ret; + } + } else { + return -ENODEV; + } + + /* + * The BQ24250 doesn't support the D+/D- based charger type detection + * used for the automatic setting of the input current limit setting so + * explicitly disable that feature. + */ + if (bq->chip == BQ24250) + bq->iilimit_autoset_enable = false; + + if (bq->iilimit_autoset_enable) + INIT_DELAYED_WORK(&bq->iilimit_setup_work, + bq24257_iilimit_setup_work); + + /* + * The BQ24250 doesn't have a dedicated Power Good (PG) pin so let's + * not probe for it and instead use a SW-based approach to determine + * the PG state. We also use a SW-based approach for all other devices + * if the PG pin is either not defined or can't be probed. + */ + if (bq->chip != BQ24250) + bq24257_pg_gpio_probe(bq); + + if (PTR_ERR(bq->pg) == -EPROBE_DEFER) + return PTR_ERR(bq->pg); + else if (!bq->pg) + dev_info(bq->dev, "using SW-based power-good detection\n"); + + /* reset all registers to defaults */ + ret = bq24257_field_write(bq, F_RESET, 1); + if (ret < 0) + return ret; + + /* + * Put the RESET bit back to 0, in cache. For some reason the HW always + * returns 1 on this bit, so this is the only way to avoid resetting the + * chip every time we update another field in this register. + */ + ret = bq24257_field_write(bq, F_RESET, 0); + if (ret < 0) + return ret; + + ret = bq24257_hw_init(bq); + if (ret < 0) { + dev_err(dev, "Cannot initialize the chip.\n"); + return ret; + } + + ret = bq24257_power_supply_init(bq); + if (ret < 0) { + dev_err(dev, "Failed to register power supply\n"); + return ret; + } + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + bq24257_irq_handler_thread, + IRQF_TRIGGER_FALLING | + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + bq2425x_chip_name[bq->chip], bq); + if (ret) { + dev_err(dev, "Failed to request IRQ #%d\n", client->irq); + return ret; + } + + return 0; +} + +static int bq24257_remove(struct i2c_client *client) +{ + struct bq24257_device *bq = i2c_get_clientdata(client); + + if (bq->iilimit_autoset_enable) + cancel_delayed_work_sync(&bq->iilimit_setup_work); + + bq24257_field_write(bq, F_RESET, 1); /* reset to defaults */ + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int bq24257_suspend(struct device *dev) +{ + struct bq24257_device *bq = dev_get_drvdata(dev); + int ret = 0; + + if (bq->iilimit_autoset_enable) + cancel_delayed_work_sync(&bq->iilimit_setup_work); + + /* reset all registers to default (and activate standalone mode) */ + ret = bq24257_field_write(bq, F_RESET, 1); + if (ret < 0) + dev_err(bq->dev, "Cannot reset chip to standalone mode.\n"); + + return ret; +} + +static int bq24257_resume(struct device *dev) +{ + int ret; + struct bq24257_device *bq = dev_get_drvdata(dev); + + ret = regcache_drop_region(bq->rmap, BQ24257_REG_1, BQ24257_REG_7); + if (ret < 0) + return ret; + + ret = bq24257_field_write(bq, F_RESET, 0); + if (ret < 0) + return ret; + + ret = bq24257_hw_init(bq); + if (ret < 0) { + dev_err(bq->dev, "Cannot init chip after resume.\n"); + return ret; + } + + /* signal userspace, maybe state changed while suspended */ + power_supply_changed(bq->charger); + + return 0; +} +#endif + +static const struct dev_pm_ops bq24257_pm = { + SET_SYSTEM_SLEEP_PM_OPS(bq24257_suspend, bq24257_resume) +}; + +static const struct i2c_device_id bq24257_i2c_ids[] = { + { "bq24250", BQ24250 }, + { "bq24251", BQ24251 }, + { "bq24257", BQ24257 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq24257_i2c_ids); + +static const struct of_device_id bq24257_of_match[] = { + { .compatible = "ti,bq24250", }, + { .compatible = "ti,bq24251", }, + { .compatible = "ti,bq24257", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bq24257_of_match); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id bq24257_acpi_match[] = { + { "BQ242500", BQ24250 }, + { "BQ242510", BQ24251 }, + { "BQ242570", BQ24257 }, + {}, +}; +MODULE_DEVICE_TABLE(acpi, bq24257_acpi_match); +#endif + +static struct i2c_driver bq24257_driver = { + .driver = { + .name = "bq24257-charger", + .of_match_table = of_match_ptr(bq24257_of_match), + .acpi_match_table = ACPI_PTR(bq24257_acpi_match), + .pm = &bq24257_pm, + }, + .probe = bq24257_probe, + .remove = bq24257_remove, + .id_table = bq24257_i2c_ids, +}; +module_i2c_driver(bq24257_driver); + +MODULE_AUTHOR("Laurentiu Palcu <laurentiu.palcu@intel.com>"); +MODULE_DESCRIPTION("bq24257 charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bq24735-charger.c b/drivers/power/supply/bq24735-charger.c new file mode 100644 index 000000000..6931e1d82 --- /dev/null +++ b/drivers/power/supply/bq24735-charger.c @@ -0,0 +1,521 @@ +/* + * Battery charger driver for TI BQ24735 + * + * Copyright (c) 2013, NVIDIA CORPORATION. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include <linux/err.h> +#include <linux/gpio.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/gpio/consumer.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#include <linux/power/bq24735-charger.h> + +#define BQ24735_CHG_OPT 0x12 +#define BQ24735_CHG_OPT_CHARGE_DISABLE (1 << 0) +#define BQ24735_CHG_OPT_AC_PRESENT (1 << 4) +#define BQ24735_CHARGE_CURRENT 0x14 +#define BQ24735_CHARGE_CURRENT_MASK 0x1fc0 +#define BQ24735_CHARGE_VOLTAGE 0x15 +#define BQ24735_CHARGE_VOLTAGE_MASK 0x7ff0 +#define BQ24735_INPUT_CURRENT 0x3f +#define BQ24735_INPUT_CURRENT_MASK 0x1f80 +#define BQ24735_MANUFACTURER_ID 0xfe +#define BQ24735_DEVICE_ID 0xff + +struct bq24735 { + struct power_supply *charger; + struct power_supply_desc charger_desc; + struct i2c_client *client; + struct bq24735_platform *pdata; + struct mutex lock; + struct gpio_desc *status_gpio; + struct delayed_work poll; + u32 poll_interval; + bool charging; +}; + +static inline struct bq24735 *to_bq24735(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static enum power_supply_property bq24735_charger_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, +}; + +static int bq24735_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + return 1; + default: + break; + } + + return 0; +} + +static inline int bq24735_write_word(struct i2c_client *client, u8 reg, + u16 value) +{ + return i2c_smbus_write_word_data(client, reg, value); +} + +static inline int bq24735_read_word(struct i2c_client *client, u8 reg) +{ + return i2c_smbus_read_word_data(client, reg); +} + +static int bq24735_update_word(struct i2c_client *client, u8 reg, + u16 mask, u16 value) +{ + unsigned int tmp; + int ret; + + ret = bq24735_read_word(client, reg); + if (ret < 0) + return ret; + + tmp = ret & ~mask; + tmp |= value & mask; + + return bq24735_write_word(client, reg, tmp); +} + +static int bq24735_config_charger(struct bq24735 *charger) +{ + struct bq24735_platform *pdata = charger->pdata; + int ret; + u16 value; + + if (pdata->ext_control) + return 0; + + if (pdata->charge_current) { + value = pdata->charge_current & BQ24735_CHARGE_CURRENT_MASK; + + ret = bq24735_write_word(charger->client, + BQ24735_CHARGE_CURRENT, value); + if (ret < 0) { + dev_err(&charger->client->dev, + "Failed to write charger current : %d\n", + ret); + return ret; + } + } + + if (pdata->charge_voltage) { + value = pdata->charge_voltage & BQ24735_CHARGE_VOLTAGE_MASK; + + ret = bq24735_write_word(charger->client, + BQ24735_CHARGE_VOLTAGE, value); + if (ret < 0) { + dev_err(&charger->client->dev, + "Failed to write charger voltage : %d\n", + ret); + return ret; + } + } + + if (pdata->input_current) { + value = pdata->input_current & BQ24735_INPUT_CURRENT_MASK; + + ret = bq24735_write_word(charger->client, + BQ24735_INPUT_CURRENT, value); + if (ret < 0) { + dev_err(&charger->client->dev, + "Failed to write input current : %d\n", + ret); + return ret; + } + } + + return 0; +} + +static inline int bq24735_enable_charging(struct bq24735 *charger) +{ + int ret; + + if (charger->pdata->ext_control) + return 0; + + ret = bq24735_config_charger(charger); + if (ret) + return ret; + + return bq24735_update_word(charger->client, BQ24735_CHG_OPT, + BQ24735_CHG_OPT_CHARGE_DISABLE, 0); +} + +static inline int bq24735_disable_charging(struct bq24735 *charger) +{ + if (charger->pdata->ext_control) + return 0; + + return bq24735_update_word(charger->client, BQ24735_CHG_OPT, + BQ24735_CHG_OPT_CHARGE_DISABLE, + BQ24735_CHG_OPT_CHARGE_DISABLE); +} + +static bool bq24735_charger_is_present(struct bq24735 *charger) +{ + if (charger->status_gpio) { + return !gpiod_get_value_cansleep(charger->status_gpio); + } else { + int ac = 0; + + ac = bq24735_read_word(charger->client, BQ24735_CHG_OPT); + if (ac < 0) { + dev_dbg(&charger->client->dev, + "Failed to read charger options : %d\n", + ac); + return false; + } + return (ac & BQ24735_CHG_OPT_AC_PRESENT) ? true : false; + } + + return false; +} + +static int bq24735_charger_is_charging(struct bq24735 *charger) +{ + int ret; + + if (!bq24735_charger_is_present(charger)) + return 0; + + ret = bq24735_read_word(charger->client, BQ24735_CHG_OPT); + if (ret < 0) + return ret; + + return !(ret & BQ24735_CHG_OPT_CHARGE_DISABLE); +} + +static void bq24735_update(struct bq24735 *charger) +{ + mutex_lock(&charger->lock); + + if (charger->charging && bq24735_charger_is_present(charger)) + bq24735_enable_charging(charger); + else + bq24735_disable_charging(charger); + + mutex_unlock(&charger->lock); + + power_supply_changed(charger->charger); +} + +static irqreturn_t bq24735_charger_isr(int irq, void *devid) +{ + struct power_supply *psy = devid; + struct bq24735 *charger = to_bq24735(psy); + + bq24735_update(charger); + + return IRQ_HANDLED; +} + +static void bq24735_poll(struct work_struct *work) +{ + struct bq24735 *charger = container_of(work, struct bq24735, poll.work); + + bq24735_update(charger); + + schedule_delayed_work(&charger->poll, + msecs_to_jiffies(charger->poll_interval)); +} + +static int bq24735_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq24735 *charger = to_bq24735(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = bq24735_charger_is_present(charger) ? 1 : 0; + break; + case POWER_SUPPLY_PROP_STATUS: + switch (bq24735_charger_is_charging(charger)) { + case 1: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case 0: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static int bq24735_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct bq24735 *charger = to_bq24735(psy); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + switch (val->intval) { + case POWER_SUPPLY_STATUS_CHARGING: + mutex_lock(&charger->lock); + charger->charging = true; + ret = bq24735_enable_charging(charger); + mutex_unlock(&charger->lock); + if (ret) + return ret; + break; + case POWER_SUPPLY_STATUS_DISCHARGING: + case POWER_SUPPLY_STATUS_NOT_CHARGING: + mutex_lock(&charger->lock); + charger->charging = false; + ret = bq24735_disable_charging(charger); + mutex_unlock(&charger->lock); + if (ret) + return ret; + break; + default: + return -EINVAL; + } + power_supply_changed(psy); + break; + default: + return -EPERM; + } + + return 0; +} + +static struct bq24735_platform *bq24735_parse_dt_data(struct i2c_client *client) +{ + struct bq24735_platform *pdata; + struct device_node *np = client->dev.of_node; + u32 val; + int ret; + + pdata = devm_kzalloc(&client->dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) { + dev_err(&client->dev, + "Memory alloc for bq24735 pdata failed\n"); + return NULL; + } + + ret = of_property_read_u32(np, "ti,charge-current", &val); + if (!ret) + pdata->charge_current = val; + + ret = of_property_read_u32(np, "ti,charge-voltage", &val); + if (!ret) + pdata->charge_voltage = val; + + ret = of_property_read_u32(np, "ti,input-current", &val); + if (!ret) + pdata->input_current = val; + + pdata->ext_control = of_property_read_bool(np, "ti,external-control"); + + return pdata; +} + +static int bq24735_charger_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct bq24735 *charger; + struct power_supply_desc *supply_desc; + struct power_supply_config psy_cfg = {}; + char *name; + + charger = devm_kzalloc(&client->dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + mutex_init(&charger->lock); + charger->charging = true; + charger->pdata = client->dev.platform_data; + + if (IS_ENABLED(CONFIG_OF) && !charger->pdata && client->dev.of_node) + charger->pdata = bq24735_parse_dt_data(client); + + if (!charger->pdata) { + dev_err(&client->dev, "no platform data provided\n"); + return -EINVAL; + } + + name = (char *)charger->pdata->name; + if (!name) { + name = devm_kasprintf(&client->dev, GFP_KERNEL, + "bq24735@%s", + dev_name(&client->dev)); + if (!name) { + dev_err(&client->dev, "Failed to alloc device name\n"); + return -ENOMEM; + } + } + + charger->client = client; + + supply_desc = &charger->charger_desc; + + supply_desc->name = name; + supply_desc->type = POWER_SUPPLY_TYPE_MAINS; + supply_desc->properties = bq24735_charger_properties; + supply_desc->num_properties = ARRAY_SIZE(bq24735_charger_properties); + supply_desc->get_property = bq24735_charger_get_property; + supply_desc->set_property = bq24735_charger_set_property; + supply_desc->property_is_writeable = + bq24735_charger_property_is_writeable; + + psy_cfg.supplied_to = charger->pdata->supplied_to; + psy_cfg.num_supplicants = charger->pdata->num_supplicants; + psy_cfg.of_node = client->dev.of_node; + psy_cfg.drv_data = charger; + + i2c_set_clientdata(client, charger); + + charger->status_gpio = devm_gpiod_get_optional(&client->dev, + "ti,ac-detect", + GPIOD_IN); + if (IS_ERR(charger->status_gpio)) { + ret = PTR_ERR(charger->status_gpio); + dev_err(&client->dev, "Getting gpio failed: %d\n", ret); + return ret; + } + + if (bq24735_charger_is_present(charger)) { + ret = bq24735_read_word(client, BQ24735_MANUFACTURER_ID); + if (ret < 0) { + dev_err(&client->dev, "Failed to read manufacturer id : %d\n", + ret); + return ret; + } else if (ret != 0x0040) { + dev_err(&client->dev, + "manufacturer id mismatch. 0x0040 != 0x%04x\n", ret); + return -ENODEV; + } + + ret = bq24735_read_word(client, BQ24735_DEVICE_ID); + if (ret < 0) { + dev_err(&client->dev, "Failed to read device id : %d\n", ret); + return ret; + } else if (ret != 0x000B) { + dev_err(&client->dev, + "device id mismatch. 0x000b != 0x%04x\n", ret); + return -ENODEV; + } + + ret = bq24735_enable_charging(charger); + if (ret < 0) { + dev_err(&client->dev, "Failed to enable charging\n"); + return ret; + } + } + + charger->charger = devm_power_supply_register(&client->dev, supply_desc, + &psy_cfg); + if (IS_ERR(charger->charger)) { + ret = PTR_ERR(charger->charger); + dev_err(&client->dev, "Failed to register power supply: %d\n", + ret); + return ret; + } + + if (client->irq) { + ret = devm_request_threaded_irq(&client->dev, client->irq, + NULL, bq24735_charger_isr, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + supply_desc->name, + charger->charger); + if (ret) { + dev_err(&client->dev, + "Unable to register IRQ %d err %d\n", + client->irq, ret); + return ret; + } + } else { + ret = device_property_read_u32(&client->dev, "poll-interval", + &charger->poll_interval); + if (ret) + return 0; + if (!charger->poll_interval) + return 0; + + INIT_DELAYED_WORK(&charger->poll, bq24735_poll); + schedule_delayed_work(&charger->poll, + msecs_to_jiffies(charger->poll_interval)); + } + + return 0; +} + +static int bq24735_charger_remove(struct i2c_client *client) +{ + struct bq24735 *charger = i2c_get_clientdata(client); + + if (charger->poll_interval) + cancel_delayed_work_sync(&charger->poll); + + return 0; +} + +static const struct i2c_device_id bq24735_charger_id[] = { + { "bq24735-charger", 0 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, bq24735_charger_id); + +static const struct of_device_id bq24735_match_ids[] = { + { .compatible = "ti,bq24735", }, + { /* end */ } +}; +MODULE_DEVICE_TABLE(of, bq24735_match_ids); + +static struct i2c_driver bq24735_charger_driver = { + .driver = { + .name = "bq24735-charger", + .of_match_table = bq24735_match_ids, + }, + .probe = bq24735_charger_probe, + .remove = bq24735_charger_remove, + .id_table = bq24735_charger_id, +}; + +module_i2c_driver(bq24735_charger_driver); + +MODULE_DESCRIPTION("bq24735 battery charging driver"); +MODULE_AUTHOR("Darbha Sriharsha <dsriharsha@nvidia.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/bq2515x_charger.c b/drivers/power/supply/bq2515x_charger.c new file mode 100644 index 000000000..374b112f7 --- /dev/null +++ b/drivers/power/supply/bq2515x_charger.c @@ -0,0 +1,1169 @@ +// SPDX-License-Identifier: GPL-2.0 +// BQ2515X Battery Charger Driver +// Copyright (C) 2020 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/gpio/consumer.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/types.h> + +#define BQ2515X_MANUFACTURER "Texas Instruments" + +#define BQ2515X_STAT0 0x00 +#define BQ2515X_STAT1 0x01 +#define BQ2515X_STAT2 0x02 +#define BQ2515X_FLAG0 0x03 +#define BQ2515X_FLAG1 0x04 +#define BQ2515X_FLAG2 0x05 +#define BQ2515X_FLAG3 0x06 +#define BQ2515X_MASK0 0x07 +#define BQ2515X_MASK1 0x08 +#define BQ2515X_MASK2 0x09 +#define BQ2515X_MASK3 0x0a +#define BQ2515X_VBAT_CTRL 0x12 +#define BQ2515X_ICHG_CTRL 0x13 +#define BQ2515X_PCHRGCTRL 0x14 +#define BQ2515X_TERMCTRL 0x15 +#define BQ2515X_BUVLO 0x16 +#define BQ2515X_CHARGERCTRL0 0x17 +#define BQ2515X_CHARGERCTRL1 0x18 +#define BQ2515X_ILIMCTRL 0x19 +#define BQ2515X_LDOCTRL 0x1d +#define BQ2515X_MRCTRL 0x30 +#define BQ2515X_ICCTRL0 0x35 +#define BQ2515X_ICCTRL1 0x36 +#define BQ2515X_ICCTRL2 0x37 +#define BQ2515X_ADCCTRL0 0x40 +#define BQ2515X_ADCCTRL1 0x41 +#define BQ2515X_ADC_VBAT_M 0x42 +#define BQ2515X_ADC_VBAT_L 0x43 +#define BQ2515X_ADC_TS_M 0x44 +#define BQ2515X_ADC_TS_L 0x45 +#define BQ2515X_ADC_ICHG_M 0x46 +#define BQ2515X_ADC_ICHG_L 0x47 +#define BQ2515X_ADC_ADCIN_M 0x48 +#define BQ2515X_ADC_ADCIN_L 0x49 +#define BQ2515X_ADC_VIN_M 0x4a +#define BQ2515X_ADC_VIN_L 0x4b +#define BQ2515X_ADC_PMID_M 0x4c +#define BQ2515X_ADC_PMID_L 0x4d +#define BQ2515X_ADC_IIN_M 0x4e +#define BQ2515X_ADC_IIN_L 0x4f +#define BQ2515X_ADC_COMP1_M 0x52 +#define BQ2515X_ADC_COMP1_L 0X53 +#define BQ2515X_ADC_COMP2_M 0X54 +#define BQ2515X_ADC_COMP2_L 0x55 +#define BQ2515X_ADC_COMP3_M 0x56 +#define BQ2515X_ADC_COMP3_L 0x57 +#define BQ2515X_ADC_READ_EN 0x58 +#define BQ2515X_TS_FASTCHGCTRL 0x61 +#define BQ2515X_TS_COLD 0x62 +#define BQ2515X_TS_COOL 0x63 +#define BQ2515X_TS_WARM 0x64 +#define BQ2515X_TS_HOT 0x65 +#define BQ2515X_DEVICE_ID 0x6f + +#define BQ2515X_DEFAULT_ICHG_UA 10000 +#define BQ25150_DEFAULT_ILIM_UA 100000 +#define BQ25155_DEFAULT_ILIM_UA 500000 +#define BQ2515X_DEFAULT_VBAT_REG_UV 4200000 +#define BQ2515X_DEFAULT_IPRECHARGE_UA 2500 + +#define BQ2515X_DIVISOR 65536 +#define BQ2515X_VBAT_BASE_VOLT 3600000 +#define BQ2515X_VBAT_REG_MAX 4600000 +#define BQ2515X_VBAT_REG_MIN 3600000 +#define BQ2515X_VBAT_STEP_UV 10000 +#define BQ2515X_UV_FACTOR 1000000 +#define BQ2515X_VBAT_MULTIPLIER 6 +#define BQ2515X_ICHG_DIVISOR 52429 +#define BQ2515X_ICHG_CURR_STEP_THRESH_UA 318750 +#define BQ2515X_ICHG_MIN_UA 0 +#define BQ2515X_ICHG_MAX_UA 500000 +#define BQ2515X_ICHG_RNG_1B0_UA 1250 +#define BQ2515X_ICHG_RNG_1B1_UA 2500 +#define BQ2515X_VLOWV_SEL_1B0_UV 3000000 +#define BQ2515X_VLOWV_SEL_1B1_UV 2800000 +#define BQ2515X_PRECHRG_ICHRG_RNGE_1875_UA 18750 +#define BQ2515X_PRECHRG_ICHRG_RNGE_3750_UA 37500 +#define BQ2515X_TWAKE2_MIN_US 1700000 +#define BQ2515X_TWAKE2_MAX_US 2300000 + +#define BQ2515X_ILIM_150MA 0x2 +#define BQ2515X_ILIM_MASK 0x7 +#define BQ2515X_ILIM_MIN 50000 +#define BQ2515X_ILIM_MAX 600000 +#define BQ2515X_HEALTH_MASK 0xf +#define BQ2515X_ICHGRNG_MASK 0x80 +#define BQ2515X_STAT0_MASK 0x0f +#define BQ2515X_STAT1_MASK 0x1f +#define BQ2515X_PRECHARGE_MASK 0x1f + +#define BQ2515X_TS_HOT_STAT BIT(0) +#define BQ2515X_TS_WARM_STAT BIT(1) +#define BQ2515X_TS_COOL_STAT BIT(2) +#define BQ2515X_TS_COLD_STAT BIT(3) +#define BQ2515X_SAFETY_TIMER_EXP BIT(5) + +#define BQ2515X_EN_VBAT_READ BIT(3) +#define BQ2515X_EN_ICHG_READ BIT(5) + +#define BQ2515X_VIN_GOOD BIT(0) +#define BQ2515X_CHRG_DONE BIT(5) +#define BQ2515X_CV_CHRG_MODE BIT(6) + +#define BQ2515X_VIN_OVP_FAULT_STAT BIT(7) + +#define BQ2515X_WATCHDOG_DISABLE BIT(4) + +#define BQ2515X_ICHARGE_RANGE BIT(7) + +#define BQ2515X_VLOWV_SEL BIT(5) + +#define BQ2515X_CHARGER_DISABLE BIT(0) + +#define BQ2515X_HWRESET_14S_WD BIT(1) + +static const int bq2515x_ilim_lvl_values[] = { + 50000, 100000, 150000, 200000, 300000, 400000, 500000, 600000 +}; + +/** + * struct bq2515x_init_data - + * @ilim: input current limit + * @ichg: fast charge current + * @vbatreg: battery regulation voltage + * @iprechg: precharge current + */ +struct bq2515x_init_data { + int ilim; + int ichg; + int vbatreg; + int iprechg; +}; + +enum bq2515x_id { + BQ25150, + BQ25155, +}; + +/** + * struct bq2515x_device - + * @mains: mains properties + * @battery: battery properties + * @regmap: register map structure + * @dev: device structure + * + * @reset_gpio: manual reset (MR) pin + * @powerdown_gpio: low power mode pin + * @ac_detect_gpio: power good (PG) pin + * @ce_gpio: charge enable (CE) pin + * + * @model_name: string value describing device model + * @device_id: value of device_id + * @mains_online: boolean value indicating power supply online + * + * @init_data: charger initialization data structure + */ +struct bq2515x_device { + struct power_supply *mains; + struct power_supply *battery; + struct regmap *regmap; + struct device *dev; + + struct gpio_desc *reset_gpio; + struct gpio_desc *powerdown_gpio; + struct gpio_desc *ac_detect_gpio; + struct gpio_desc *ce_gpio; + + char model_name[I2C_NAME_SIZE]; + int device_id; + bool mains_online; + + struct bq2515x_init_data init_data; +}; + +static const struct reg_default bq25150_reg_defaults[] = { + {BQ2515X_FLAG0, 0x0}, + {BQ2515X_FLAG1, 0x0}, + {BQ2515X_FLAG2, 0x0}, + {BQ2515X_FLAG3, 0x0}, + {BQ2515X_MASK0, 0x0}, + {BQ2515X_MASK1, 0x0}, + {BQ2515X_MASK2, 0x71}, + {BQ2515X_MASK3, 0x0}, + {BQ2515X_VBAT_CTRL, 0x3C}, + {BQ2515X_ICHG_CTRL, 0x8}, + {BQ2515X_PCHRGCTRL, 0x2}, + {BQ2515X_TERMCTRL, 0x14}, + {BQ2515X_BUVLO, 0x0}, + {BQ2515X_CHARGERCTRL0, 0x82}, + {BQ2515X_CHARGERCTRL1, 0x42}, + {BQ2515X_ILIMCTRL, 0x1}, + {BQ2515X_LDOCTRL, 0xB0}, + {BQ2515X_MRCTRL, 0x2A}, + {BQ2515X_ICCTRL0, 0x10}, + {BQ2515X_ICCTRL1, 0x0}, + {BQ2515X_ICCTRL2, 0x0}, + {BQ2515X_ADCCTRL0, 0x2}, + {BQ2515X_ADCCTRL1, 0x40}, + {BQ2515X_ADC_COMP1_M, 0x23}, + {BQ2515X_ADC_COMP1_L, 0x20}, + {BQ2515X_ADC_COMP2_M, 0x38}, + {BQ2515X_ADC_COMP2_L, 0x90}, + {BQ2515X_ADC_COMP3_M, 0x0}, + {BQ2515X_ADC_COMP3_L, 0x0}, + {BQ2515X_ADC_READ_EN, 0x0}, + {BQ2515X_TS_FASTCHGCTRL, 0x34}, + {BQ2515X_TS_COLD, 0x7C}, + {BQ2515X_TS_COOL, 0x6D}, + {BQ2515X_TS_WARM, 0x38}, + {BQ2515X_TS_HOT, 0x27}, + {BQ2515X_DEVICE_ID, 0x20}, +}; + +static const struct reg_default bq25155_reg_defaults[] = { + {BQ2515X_FLAG0, 0x0}, + {BQ2515X_FLAG1, 0x0}, + {BQ2515X_FLAG2, 0x0}, + {BQ2515X_FLAG3, 0x0}, + {BQ2515X_MASK0, 0x0}, + {BQ2515X_MASK1, 0x0}, + {BQ2515X_MASK2, 0x71}, + {BQ2515X_MASK3, 0x0}, + {BQ2515X_VBAT_CTRL, 0x3C}, + {BQ2515X_ICHG_CTRL, 0x8}, + {BQ2515X_PCHRGCTRL, 0x2}, + {BQ2515X_TERMCTRL, 0x14}, + {BQ2515X_BUVLO, 0x0}, + {BQ2515X_CHARGERCTRL0, 0x82}, + {BQ2515X_CHARGERCTRL1, 0xC2}, + {BQ2515X_ILIMCTRL, 0x6}, + {BQ2515X_LDOCTRL, 0xB0}, + {BQ2515X_MRCTRL, 0x2A}, + {BQ2515X_ICCTRL0, 0x10}, + {BQ2515X_ICCTRL1, 0x0}, + {BQ2515X_ICCTRL2, 0x40}, + {BQ2515X_ADCCTRL0, 0x2}, + {BQ2515X_ADCCTRL1, 0x40}, + {BQ2515X_ADC_COMP1_M, 0x23}, + {BQ2515X_ADC_COMP1_L, 0x20}, + {BQ2515X_ADC_COMP2_M, 0x38}, + {BQ2515X_ADC_COMP2_L, 0x90}, + {BQ2515X_ADC_COMP3_M, 0x0}, + {BQ2515X_ADC_COMP3_L, 0x0}, + {BQ2515X_ADC_READ_EN, 0x0}, + {BQ2515X_TS_FASTCHGCTRL, 0x34}, + {BQ2515X_TS_COLD, 0x7C}, + {BQ2515X_TS_COOL, 0x6D}, + {BQ2515X_TS_WARM, 0x38}, + {BQ2515X_TS_HOT, 0x27}, + {BQ2515X_DEVICE_ID, 0x35}, +}; + +static int bq2515x_wake_up(struct bq2515x_device *bq2515x) +{ + int ret; + int val; + + /* Read the STAT register if we can read it then the device is out + * of ship mode. If the register cannot be read then attempt to wake + * it up and enable the ADC. + */ + ret = regmap_read(bq2515x->regmap, BQ2515X_STAT0, &val); + if (ret) + return ret; + + /* Need to toggle LP and bring device out of ship mode. The device + * will exit the ship mode when the MR pin is held low for at least + * t_WAKE2 as shown in section 8.3.7.1 of the datasheet. + */ + gpiod_set_value_cansleep(bq2515x->powerdown_gpio, 0); + + gpiod_set_value_cansleep(bq2515x->reset_gpio, 0); + usleep_range(BQ2515X_TWAKE2_MIN_US, BQ2515X_TWAKE2_MAX_US); + gpiod_set_value_cansleep(bq2515x->reset_gpio, 1); + + return regmap_write(bq2515x->regmap, BQ2515X_ADC_READ_EN, + (BQ2515X_EN_VBAT_READ | BQ2515X_EN_ICHG_READ)); +} + +static int bq2515x_update_ps_status(struct bq2515x_device *bq2515x) +{ + bool dc = false; + unsigned int val; + int ret; + + if (bq2515x->ac_detect_gpio) + val = gpiod_get_value_cansleep(bq2515x->ac_detect_gpio); + else { + ret = regmap_read(bq2515x->regmap, BQ2515X_STAT0, &val); + if (ret) + return ret; + } + + dc = val & BQ2515X_VIN_GOOD; + + ret = bq2515x->mains_online != dc; + + bq2515x->mains_online = dc; + + return ret; +} + +static int bq2515x_disable_watchdog_timers(struct bq2515x_device *bq2515x) +{ + int ret; + + ret = regmap_update_bits(bq2515x->regmap, BQ2515X_CHARGERCTRL0, + BQ2515X_WATCHDOG_DISABLE, BQ2515X_WATCHDOG_DISABLE); + if (ret) + return ret; + + return regmap_update_bits(bq2515x->regmap, BQ2515X_ICCTRL2, + BQ2515X_HWRESET_14S_WD, 0); +} + +static int bq2515x_get_battery_voltage_now(struct bq2515x_device *bq2515x) +{ + int ret; + int vbat_msb; + int vbat_lsb; + uint32_t vbat_measurement; + + if (!bq2515x->mains_online) + bq2515x_wake_up(bq2515x); + + ret = regmap_read(bq2515x->regmap, BQ2515X_ADC_VBAT_M, &vbat_msb); + if (ret) + return ret; + + ret = regmap_read(bq2515x->regmap, BQ2515X_ADC_VBAT_L, &vbat_lsb); + if (ret) + return ret; + + vbat_measurement = (vbat_msb << 8) | vbat_lsb; + + return vbat_measurement * (BQ2515X_UV_FACTOR / BQ2515X_DIVISOR) * + BQ2515X_VBAT_MULTIPLIER; +} + +static int bq2515x_get_battery_current_now(struct bq2515x_device *bq2515x) +{ + int ret; + int ichg_msb; + int ichg_lsb; + uint32_t ichg_measurement; + u16 ichg_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + unsigned int ichg_reg_code, reg_code; + unsigned int icharge_range = 0, pchrgctrl; + unsigned int buvlo, vlowv_sel, vlowv = BQ2515X_VLOWV_SEL_1B0_UV; + + if (!bq2515x->mains_online) + return -ENODATA; + + ret = regmap_read(bq2515x->regmap, BQ2515X_ADC_ICHG_M, &ichg_msb); + if (ret) + return ret; + + ret = regmap_read(bq2515x->regmap, BQ2515X_ADC_ICHG_L, &ichg_lsb); + if (ret) + return ret; + + ichg_measurement = (ichg_msb << 8) | ichg_lsb; + + ret = regmap_read(bq2515x->regmap, BQ2515X_BUVLO, &buvlo); + if (ret) + return ret; + + vlowv_sel = buvlo & BQ2515X_VLOWV_SEL; + + if (vlowv_sel) + vlowv = BQ2515X_VLOWV_SEL_1B1_UV; + + if (bq2515x_get_battery_voltage_now(bq2515x) < vlowv) { + ret = regmap_read(bq2515x->regmap, BQ2515X_PCHRGCTRL, + &pchrgctrl); + if (ret) + return ret; + + reg_code = pchrgctrl & BQ2515X_PRECHARGE_MASK; + } else { + ret = regmap_read(bq2515x->regmap, BQ2515X_ICHG_CTRL, + &ichg_reg_code); + if (ret) + return ret; + + reg_code = ichg_reg_code; + } + + ret = regmap_read(bq2515x->regmap, BQ2515X_PCHRGCTRL, &pchrgctrl); + if (ret) + return ret; + + icharge_range = pchrgctrl & BQ2515X_ICHARGE_RANGE; + + if (icharge_range) + ichg_multiplier = BQ2515X_ICHG_RNG_1B1_UA; + + return reg_code * (ichg_multiplier * ichg_measurement / + BQ2515X_ICHG_DIVISOR); +} + +static bool bq2515x_get_charge_disable(struct bq2515x_device *bq2515x) +{ + int ret; + int ce_pin; + int icctrl2; + int charger_disable; + + ce_pin = gpiod_get_value_cansleep(bq2515x->ce_gpio); + + ret = regmap_read(bq2515x->regmap, BQ2515X_ICCTRL2, &icctrl2); + if (ret) + return ret; + + charger_disable = icctrl2 & BQ2515X_CHARGER_DISABLE; + + if (charger_disable || ce_pin) + return true; + + return false; +} + +static int bq2515x_set_charge_disable(struct bq2515x_device *bq2515x, int val) +{ + gpiod_set_value_cansleep(bq2515x->ce_gpio, val); + + return regmap_update_bits(bq2515x->regmap, BQ2515X_ICCTRL2, + BQ2515X_CHARGER_DISABLE, val); +} + +static int bq2515x_get_const_charge_current(struct bq2515x_device *bq2515x) +{ + int ret; + u16 ichg_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + unsigned int ichg_reg_code; + unsigned int pchrgctrl; + unsigned int icharge_range; + + ret = regmap_read(bq2515x->regmap, BQ2515X_ICHG_CTRL, &ichg_reg_code); + if (ret) + return ret; + + ret = regmap_read(bq2515x->regmap, BQ2515X_PCHRGCTRL, &pchrgctrl); + if (ret) + return ret; + + icharge_range = pchrgctrl & BQ2515X_ICHARGE_RANGE; + + if (icharge_range) + ichg_multiplier = BQ2515X_ICHG_RNG_1B1_UA; + + return ichg_reg_code * ichg_multiplier; +} + +static int bq2515x_set_const_charge_current(struct bq2515x_device *bq2515x, + int val) +{ + int ret; + unsigned int ichg_reg_code; + u16 ichg_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + unsigned int icharge_range = 0; + + if (val > BQ2515X_ICHG_MAX_UA || val < BQ2515X_ICHG_MIN_UA) + return -EINVAL; + + if (val > BQ2515X_ICHG_CURR_STEP_THRESH_UA) { + ichg_multiplier = BQ2515X_ICHG_RNG_1B1_UA; + icharge_range = BQ2515X_ICHARGE_RANGE; + } + + bq2515x_set_charge_disable(bq2515x, 1); + + ret = regmap_update_bits(bq2515x->regmap, BQ2515X_PCHRGCTRL, + BQ2515X_ICHARGE_RANGE, icharge_range); + if (ret) + return ret; + + ichg_reg_code = val / ichg_multiplier; + + ret = regmap_write(bq2515x->regmap, BQ2515X_ICHG_CTRL, ichg_reg_code); + if (ret) + return ret; + + return bq2515x_set_charge_disable(bq2515x, 0); +} + +static int bq2515x_get_precharge_current(struct bq2515x_device *bq2515x) +{ + int ret; + unsigned int pchrgctrl; + unsigned int icharge_range; + u16 precharge_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + unsigned int precharge_reg_code; + + ret = regmap_read(bq2515x->regmap, BQ2515X_PCHRGCTRL, &pchrgctrl); + if (ret) + return ret; + + icharge_range = pchrgctrl & BQ2515X_ICHARGE_RANGE; + + if (icharge_range) + precharge_multiplier = BQ2515X_ICHG_RNG_1B1_UA; + + precharge_reg_code = pchrgctrl & BQ2515X_PRECHARGE_MASK; + + return precharge_reg_code * precharge_multiplier; +} + +static int bq2515x_set_precharge_current(struct bq2515x_device *bq2515x, + int val) +{ + int ret; + unsigned int pchrgctrl; + unsigned int icharge_range; + unsigned int precharge_reg_code; + unsigned int precharge_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + unsigned int precharge_max_ua = BQ2515X_PRECHRG_ICHRG_RNGE_1875_UA; + + ret = regmap_read(bq2515x->regmap, BQ2515X_PCHRGCTRL, &pchrgctrl); + if (ret) + return ret; + + icharge_range = pchrgctrl & BQ2515X_ICHARGE_RANGE; + + if (icharge_range) { + precharge_max_ua = BQ2515X_PRECHRG_ICHRG_RNGE_3750_UA; + precharge_multiplier = BQ2515X_ICHG_RNG_1B1_UA; + } else { + precharge_max_ua = BQ2515X_PRECHRG_ICHRG_RNGE_1875_UA; + precharge_multiplier = BQ2515X_ICHG_RNG_1B0_UA; + } + if (val > precharge_max_ua || val < BQ2515X_ICHG_MIN_UA) + return -EINVAL; + + precharge_reg_code = val / precharge_multiplier; + + ret = bq2515x_set_charge_disable(bq2515x, 1); + if (ret) + return ret; + + ret = regmap_update_bits(bq2515x->regmap, BQ2515X_PCHRGCTRL, + BQ2515X_PRECHARGE_MASK, precharge_reg_code); + if (ret) + return ret; + + return bq2515x_set_charge_disable(bq2515x, 0); +} + +static int bq2515x_charging_status(struct bq2515x_device *bq2515x, + union power_supply_propval *val) +{ + bool status0_no_fault; + bool status1_no_fault; + bool ce_status; + bool charge_done; + unsigned int status; + int ret; + + if (!bq2515x->mains_online) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + ret = regmap_read(bq2515x->regmap, BQ2515X_STAT0, &status); + if (ret) + return ret; + + /* + * The code block below is used to determine if any faults from the + * STAT0 register are disbaling charging or if the charge has completed + * according to the CHARGE_DONE_STAT bit. + */ + if (((status & BQ2515X_STAT0_MASK) == true) & + ((status & BQ2515X_CHRG_DONE) == false)) { + status0_no_fault = true; + charge_done = false; + } else if (status & BQ2515X_CHRG_DONE) { + charge_done = true; + status0_no_fault = false; + } else { + status0_no_fault = false; + charge_done = false; + } + + ret = regmap_read(bq2515x->regmap, BQ2515X_STAT1, &status); + if (ret) + return ret; + /* + * The code block below is used to determine if any faults from the + * STAT1 register are disbaling charging + */ + if ((status & BQ2515X_STAT1_MASK) == false) + status1_no_fault = true; + else + status1_no_fault = false; + + ce_status = (!bq2515x_get_charge_disable(bq2515x)); + + /* + * If there are no faults and charging is enabled, then status is + * charging. Otherwise, if charging is complete, then status is full. + * Otherwise, if a fault exists or charging is disabled, then status is + * not charging + */ + if (status0_no_fault & status1_no_fault & ce_status) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (charge_done) + val->intval = POWER_SUPPLY_STATUS_FULL; + else if (!(status0_no_fault & status1_no_fault & ce_status)) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + + return 0; +} + +static int bq2515x_get_batt_reg(struct bq2515x_device *bq2515x) +{ + int vbat_reg_code; + int ret; + + ret = regmap_read(bq2515x->regmap, BQ2515X_VBAT_CTRL, &vbat_reg_code); + if (ret) + return ret; + + return BQ2515X_VBAT_BASE_VOLT + vbat_reg_code * BQ2515X_VBAT_STEP_UV; +} + +static int bq2515x_set_batt_reg(struct bq2515x_device *bq2515x, int val) +{ + int vbat_reg_code; + + if (val > BQ2515X_VBAT_REG_MAX || val < BQ2515X_VBAT_REG_MIN) + return -EINVAL; + + vbat_reg_code = (val - BQ2515X_VBAT_BASE_VOLT) / BQ2515X_VBAT_STEP_UV; + + return regmap_write(bq2515x->regmap, BQ2515X_VBAT_CTRL, vbat_reg_code); +} + +static int bq2515x_get_ilim_lvl(struct bq2515x_device *bq2515x) +{ + int ret; + int ilimctrl; + + ret = regmap_read(bq2515x->regmap, BQ2515X_ILIMCTRL, &ilimctrl); + if (ret) + return ret; + + return bq2515x_ilim_lvl_values[ilimctrl & BQ2515X_ILIM_MASK]; +} + +static int bq2515x_set_ilim_lvl(struct bq2515x_device *bq2515x, int val) +{ + int i = 0; + unsigned int array_size = ARRAY_SIZE(bq2515x_ilim_lvl_values); + + for (i = array_size - 1; i > 0; i--) { + if (val >= bq2515x_ilim_lvl_values[i]) + break; + } + return regmap_write(bq2515x->regmap, BQ2515X_ILIMCTRL, i); +} + +static int bq2515x_power_supply_property_is_writeable(struct power_supply *psy, + enum power_supply_property prop) +{ + switch (prop) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + return true; + default: + return false; + } +} + +static int bq2515x_charger_get_health(struct bq2515x_device *bq2515x, + union power_supply_propval *val) +{ + int health = POWER_SUPPLY_HEALTH_GOOD; + int ret; + unsigned int stat1; + unsigned int flag3; + + if (!bq2515x->mains_online) + bq2515x_wake_up(bq2515x); + + ret = regmap_read(bq2515x->regmap, BQ2515X_FLAG3, &flag3); + if (ret) + return ret; + + ret = regmap_read(bq2515x->regmap, BQ2515X_STAT1, &stat1); + if (ret) + return ret; + + if (stat1 & BQ2515X_HEALTH_MASK) { + switch (stat1 & BQ2515X_HEALTH_MASK) { + case BQ2515X_TS_HOT_STAT: + health = POWER_SUPPLY_HEALTH_HOT; + break; + case BQ2515X_TS_WARM_STAT: + health = POWER_SUPPLY_HEALTH_WARM; + break; + case BQ2515X_TS_COOL_STAT: + health = POWER_SUPPLY_HEALTH_COOL; + break; + case BQ2515X_TS_COLD_STAT: + health = POWER_SUPPLY_HEALTH_COLD; + break; + default: + health = POWER_SUPPLY_HEALTH_UNKNOWN; + break; + } + } + + if (stat1 & BQ2515X_VIN_OVP_FAULT_STAT) + health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + + if (flag3 & BQ2515X_SAFETY_TIMER_EXP) + health = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + + val->intval = health; + return 0; +} + +static int bq2515x_mains_set_property(struct power_supply *psy, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct bq2515x_device *bq2515x = power_supply_get_drvdata(psy); + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq2515x_set_batt_reg(bq2515x, val->intval); + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq2515x_set_const_charge_current(bq2515x, val->intval); + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq2515x_set_ilim_lvl(bq2515x, val->intval); + break; + + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + ret = bq2515x_set_precharge_current(bq2515x, val->intval); + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int bq2515x_mains_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct bq2515x_device *bq2515x = power_supply_get_drvdata(psy); + int ret = 0; + + switch (prop) { + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq2515x_get_const_charge_current(bq2515x); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq2515x_get_batt_reg(bq2515x); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + ret = bq2515x_get_precharge_current(bq2515x); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_ONLINE: + val->intval = bq2515x->mains_online; + break; + + case POWER_SUPPLY_PROP_HEALTH: + ret = bq2515x_charger_get_health(bq2515x, val); + if (ret) + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq2515x_get_ilim_lvl(bq2515x); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bq2515x->model_name; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ2515X_MANUFACTURER; + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = bq2515x_charging_status(bq2515x, val); + if (ret) + return ret; + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int bq2515x_battery_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct bq2515x_device *bq2515x = power_supply_get_drvdata(psy); + int ret; + + ret = bq2515x_update_ps_status(bq2515x); + if (ret) + return ret; + + switch (prop) { + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + ret = bq2515x->init_data.vbatreg; + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + ret = bq2515x->init_data.ichg; + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = bq2515x_get_battery_voltage_now(bq2515x); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = bq2515x_get_battery_current_now(bq2515x); + if (ret < 0) + return ret; + val->intval = ret; + break; + + default: + return -EINVAL; + } + return 0; +} + +static const enum power_supply_property bq2515x_battery_properties[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, +}; + +static const enum power_supply_property bq2515x_mains_properties[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_PRECHARGE_CURRENT, +}; + +static const struct power_supply_desc bq2515x_mains_desc = { + .name = "bq2515x-mains", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = bq2515x_mains_get_property, + .set_property = bq2515x_mains_set_property, + .properties = bq2515x_mains_properties, + .num_properties = ARRAY_SIZE(bq2515x_mains_properties), + .property_is_writeable = bq2515x_power_supply_property_is_writeable, +}; + +static const struct power_supply_desc bq2515x_battery_desc = { + .name = "bq2515x-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = bq2515x_battery_get_property, + .properties = bq2515x_battery_properties, + .num_properties = ARRAY_SIZE(bq2515x_battery_properties), + .property_is_writeable = bq2515x_power_supply_property_is_writeable, +}; + +static int bq2515x_power_supply_register(struct bq2515x_device *bq2515x, + struct device *dev, struct power_supply_config psy_cfg) +{ + bq2515x->mains = devm_power_supply_register(bq2515x->dev, + &bq2515x_mains_desc, + &psy_cfg); + if (IS_ERR(bq2515x->mains)) + return -EINVAL; + + bq2515x->battery = devm_power_supply_register(bq2515x->dev, + &bq2515x_battery_desc, + &psy_cfg); + if (IS_ERR(bq2515x->battery)) + return -EINVAL; + + return 0; +} + +static int bq2515x_hw_init(struct bq2515x_device *bq2515x) +{ + int ret; + struct power_supply_battery_info bat_info = { }; + + ret = bq2515x_disable_watchdog_timers(bq2515x); + if (ret) + return ret; + + if (bq2515x->init_data.ilim) { + ret = bq2515x_set_ilim_lvl(bq2515x, bq2515x->init_data.ilim); + if (ret) + return ret; + } + + ret = power_supply_get_battery_info(bq2515x->mains, &bat_info); + if (ret) { + dev_warn(bq2515x->dev, "battery info missing, default values will be applied\n"); + + bq2515x->init_data.ichg = BQ2515X_DEFAULT_ICHG_UA; + + bq2515x->init_data.vbatreg = BQ2515X_DEFAULT_VBAT_REG_UV; + + bq2515x->init_data.iprechg = BQ2515X_DEFAULT_IPRECHARGE_UA; + + } else { + bq2515x->init_data.ichg = + bat_info.constant_charge_current_max_ua; + + bq2515x->init_data.vbatreg = + bat_info.constant_charge_voltage_max_uv; + + bq2515x->init_data.iprechg = + bat_info.precharge_current_ua; + } + + ret = bq2515x_set_const_charge_current(bq2515x, + bq2515x->init_data.ichg); + if (ret) + return ret; + + ret = bq2515x_set_batt_reg(bq2515x, bq2515x->init_data.vbatreg); + if (ret) + return ret; + + return bq2515x_set_precharge_current(bq2515x, + bq2515x->init_data.iprechg); +} + +static int bq2515x_read_properties(struct bq2515x_device *bq2515x) +{ + int ret; + + ret = device_property_read_u32(bq2515x->dev, + "input-current-limit-microamp", + &bq2515x->init_data.ilim); + if (ret) { + switch (bq2515x->device_id) { + case BQ25150: + bq2515x->init_data.ilim = BQ25150_DEFAULT_ILIM_UA; + break; + case BQ25155: + bq2515x->init_data.ilim = BQ25155_DEFAULT_ILIM_UA; + break; + } + } + + bq2515x->ac_detect_gpio = devm_gpiod_get_optional(bq2515x->dev, + "ac-detect", GPIOD_IN); + if (IS_ERR(bq2515x->ac_detect_gpio)) { + ret = PTR_ERR(bq2515x->ac_detect_gpio); + dev_err(bq2515x->dev, "Failed to get ac detect"); + return ret; + } + + bq2515x->reset_gpio = devm_gpiod_get_optional(bq2515x->dev, + "reset", GPIOD_OUT_LOW); + if (IS_ERR(bq2515x->reset_gpio)) { + ret = PTR_ERR(bq2515x->reset_gpio); + dev_err(bq2515x->dev, "Failed to get reset"); + return ret; + } + + bq2515x->powerdown_gpio = devm_gpiod_get_optional(bq2515x->dev, + "powerdown", GPIOD_OUT_LOW); + if (IS_ERR(bq2515x->powerdown_gpio)) { + ret = PTR_ERR(bq2515x->powerdown_gpio); + dev_err(bq2515x->dev, "Failed to get powerdown"); + return ret; + } + + bq2515x->ce_gpio = devm_gpiod_get_optional(bq2515x->dev, + "charge-enable", + GPIOD_OUT_LOW); + if (IS_ERR(bq2515x->ce_gpio)) { + ret = PTR_ERR(bq2515x->ce_gpio); + dev_err(bq2515x->dev, "Failed to get ce"); + return ret; + } + + return 0; +} + +static bool bq2515x_volatile_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case BQ2515X_STAT0 ... BQ2515X_FLAG3: + case BQ2515X_ADC_VBAT_M ... BQ2515X_ADC_IIN_L: + return true; + default: + return false; + } +} + +static const struct regmap_config bq25150_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ2515X_DEVICE_ID, + .reg_defaults = bq25150_reg_defaults, + .num_reg_defaults = ARRAY_SIZE(bq25150_reg_defaults), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = bq2515x_volatile_register, +}; + +static const struct regmap_config bq25155_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ2515X_DEVICE_ID, + .reg_defaults = bq25155_reg_defaults, + .num_reg_defaults = ARRAY_SIZE(bq25155_reg_defaults), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = bq2515x_volatile_register, +}; + +static int bq2515x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct bq2515x_device *bq2515x; + struct power_supply_config charger_cfg = {}; + int ret; + + bq2515x = devm_kzalloc(dev, sizeof(*bq2515x), GFP_KERNEL); + if (!bq2515x) + return -ENOMEM; + + bq2515x->dev = dev; + + strncpy(bq2515x->model_name, id->name, I2C_NAME_SIZE); + + bq2515x->device_id = id->driver_data; + + switch (bq2515x->device_id) { + case BQ25150: + bq2515x->regmap = devm_regmap_init_i2c(client, + &bq25150_regmap_config); + break; + case BQ25155: + bq2515x->regmap = devm_regmap_init_i2c(client, + &bq25155_regmap_config); + break; + } + + if (IS_ERR(bq2515x->regmap)) { + dev_err(dev, "failed to allocate register map\n"); + return PTR_ERR(bq2515x->regmap); + } + + i2c_set_clientdata(client, bq2515x); + + charger_cfg.drv_data = bq2515x; + charger_cfg.of_node = dev->of_node; + + ret = bq2515x_read_properties(bq2515x); + if (ret) { + dev_err(dev, "Failed to read device tree properties %d\n", + ret); + return ret; + } + + ret = bq2515x_power_supply_register(bq2515x, dev, charger_cfg); + if (ret) { + dev_err(dev, "failed to register power supply\n"); + return ret; + } + + ret = bq2515x_hw_init(bq2515x); + if (ret) { + dev_err(dev, "Cannot initialize the chip\n"); + return ret; + } + + return 0; +} + +static const struct i2c_device_id bq2515x_i2c_ids[] = { + { "bq25150", BQ25150, }, + { "bq25155", BQ25155, }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq2515x_i2c_ids); + +static const struct of_device_id bq2515x_of_match[] = { + { .compatible = "ti,bq25150", }, + { .compatible = "ti,bq25155", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bq2515x_of_match); + +static struct i2c_driver bq2515x_driver = { + .driver = { + .name = "bq2515x-charger", + .of_match_table = bq2515x_of_match, + }, + .probe = bq2515x_probe, + .id_table = bq2515x_i2c_ids, +}; +module_i2c_driver(bq2515x_driver); + +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_AUTHOR("Ricardo Rivera-Matos <r-rivera-matos@ti.com>"); +MODULE_DESCRIPTION("BQ2515X charger driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/bq25890_charger.c b/drivers/power/supply/bq25890_charger.c new file mode 100644 index 000000000..fe814805c --- /dev/null +++ b/drivers/power/supply/bq25890_charger.c @@ -0,0 +1,1110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * TI BQ25890 charger driver + * + * Copyright (C) 2015 Intel Corporation + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/types.h> +#include <linux/gpio/consumer.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/usb/phy.h> + +#include <linux/acpi.h> +#include <linux/of.h> + +#define BQ25890_MANUFACTURER "Texas Instruments" +#define BQ25890_IRQ_PIN "bq25890_irq" + +#define BQ25890_ID 3 +#define BQ25895_ID 7 +#define BQ25896_ID 0 + +enum bq25890_chip_version { + BQ25890, + BQ25892, + BQ25895, + BQ25896, +}; + +static const char *const bq25890_chip_name[] = { + "BQ25890", + "BQ25892", + "BQ25895", + "BQ25896", +}; + +enum bq25890_fields { + F_EN_HIZ, F_EN_ILIM, F_IILIM, /* Reg00 */ + F_BHOT, F_BCOLD, F_VINDPM_OFS, /* Reg01 */ + F_CONV_START, F_CONV_RATE, F_BOOSTF, F_ICO_EN, + F_HVDCP_EN, F_MAXC_EN, F_FORCE_DPM, F_AUTO_DPDM_EN, /* Reg02 */ + F_BAT_LOAD_EN, F_WD_RST, F_OTG_CFG, F_CHG_CFG, F_SYSVMIN, + F_MIN_VBAT_SEL, /* Reg03 */ + F_PUMPX_EN, F_ICHG, /* Reg04 */ + F_IPRECHG, F_ITERM, /* Reg05 */ + F_VREG, F_BATLOWV, F_VRECHG, /* Reg06 */ + F_TERM_EN, F_STAT_DIS, F_WD, F_TMR_EN, F_CHG_TMR, + F_JEITA_ISET, /* Reg07 */ + F_BATCMP, F_VCLAMP, F_TREG, /* Reg08 */ + F_FORCE_ICO, F_TMR2X_EN, F_BATFET_DIS, F_JEITA_VSET, + F_BATFET_DLY, F_BATFET_RST_EN, F_PUMPX_UP, F_PUMPX_DN, /* Reg09 */ + F_BOOSTV, F_PFM_OTG_DIS, F_BOOSTI, /* Reg0A */ + F_VBUS_STAT, F_CHG_STAT, F_PG_STAT, F_SDP_STAT, F_0B_RSVD, + F_VSYS_STAT, /* Reg0B */ + F_WD_FAULT, F_BOOST_FAULT, F_CHG_FAULT, F_BAT_FAULT, + F_NTC_FAULT, /* Reg0C */ + F_FORCE_VINDPM, F_VINDPM, /* Reg0D */ + F_THERM_STAT, F_BATV, /* Reg0E */ + F_SYSV, /* Reg0F */ + F_TSPCT, /* Reg10 */ + F_VBUS_GD, F_VBUSV, /* Reg11 */ + F_ICHGR, /* Reg12 */ + F_VDPM_STAT, F_IDPM_STAT, F_IDPM_LIM, /* Reg13 */ + F_REG_RST, F_ICO_OPTIMIZED, F_PN, F_TS_PROFILE, F_DEV_REV, /* Reg14 */ + + F_MAX_FIELDS +}; + +/* initial field values, converted to register values */ +struct bq25890_init_data { + u8 ichg; /* charge current */ + u8 vreg; /* regulation voltage */ + u8 iterm; /* termination current */ + u8 iprechg; /* precharge current */ + u8 sysvmin; /* minimum system voltage limit */ + u8 boostv; /* boost regulation voltage */ + u8 boosti; /* boost current limit */ + u8 boostf; /* boost frequency */ + u8 ilim_en; /* enable ILIM pin */ + u8 treg; /* thermal regulation threshold */ + u8 rbatcomp; /* IBAT sense resistor value */ + u8 vclamp; /* IBAT compensation voltage limit */ +}; + +struct bq25890_state { + u8 online; + u8 chrg_status; + u8 chrg_fault; + u8 vsys_status; + u8 boost_fault; + u8 bat_fault; +}; + +struct bq25890_device { + struct i2c_client *client; + struct device *dev; + struct power_supply *charger; + + struct usb_phy *usb_phy; + struct notifier_block usb_nb; + struct work_struct usb_work; + unsigned long usb_event; + + struct regmap *rmap; + struct regmap_field *rmap_fields[F_MAX_FIELDS]; + + enum bq25890_chip_version chip_version; + struct bq25890_init_data init_data; + struct bq25890_state state; + + struct mutex lock; /* protect state data */ +}; + +static const struct regmap_range bq25890_readonly_reg_ranges[] = { + regmap_reg_range(0x0b, 0x0c), + regmap_reg_range(0x0e, 0x13), +}; + +static const struct regmap_access_table bq25890_writeable_regs = { + .no_ranges = bq25890_readonly_reg_ranges, + .n_no_ranges = ARRAY_SIZE(bq25890_readonly_reg_ranges), +}; + +static const struct regmap_range bq25890_volatile_reg_ranges[] = { + regmap_reg_range(0x00, 0x00), + regmap_reg_range(0x02, 0x02), + regmap_reg_range(0x09, 0x09), + regmap_reg_range(0x0b, 0x14), +}; + +static const struct regmap_access_table bq25890_volatile_regs = { + .yes_ranges = bq25890_volatile_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(bq25890_volatile_reg_ranges), +}; + +static const struct regmap_config bq25890_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = 0x14, + .cache_type = REGCACHE_RBTREE, + + .wr_table = &bq25890_writeable_regs, + .volatile_table = &bq25890_volatile_regs, +}; + +static const struct reg_field bq25890_reg_fields[] = { + /* REG00 */ + [F_EN_HIZ] = REG_FIELD(0x00, 7, 7), + [F_EN_ILIM] = REG_FIELD(0x00, 6, 6), + [F_IILIM] = REG_FIELD(0x00, 0, 5), + /* REG01 */ + [F_BHOT] = REG_FIELD(0x01, 6, 7), + [F_BCOLD] = REG_FIELD(0x01, 5, 5), + [F_VINDPM_OFS] = REG_FIELD(0x01, 0, 4), + /* REG02 */ + [F_CONV_START] = REG_FIELD(0x02, 7, 7), + [F_CONV_RATE] = REG_FIELD(0x02, 6, 6), + [F_BOOSTF] = REG_FIELD(0x02, 5, 5), + [F_ICO_EN] = REG_FIELD(0x02, 4, 4), + [F_HVDCP_EN] = REG_FIELD(0x02, 3, 3), // reserved on BQ25896 + [F_MAXC_EN] = REG_FIELD(0x02, 2, 2), // reserved on BQ25896 + [F_FORCE_DPM] = REG_FIELD(0x02, 1, 1), + [F_AUTO_DPDM_EN] = REG_FIELD(0x02, 0, 0), + /* REG03 */ + [F_BAT_LOAD_EN] = REG_FIELD(0x03, 7, 7), + [F_WD_RST] = REG_FIELD(0x03, 6, 6), + [F_OTG_CFG] = REG_FIELD(0x03, 5, 5), + [F_CHG_CFG] = REG_FIELD(0x03, 4, 4), + [F_SYSVMIN] = REG_FIELD(0x03, 1, 3), + [F_MIN_VBAT_SEL] = REG_FIELD(0x03, 0, 0), // BQ25896 only + /* REG04 */ + [F_PUMPX_EN] = REG_FIELD(0x04, 7, 7), + [F_ICHG] = REG_FIELD(0x04, 0, 6), + /* REG05 */ + [F_IPRECHG] = REG_FIELD(0x05, 4, 7), + [F_ITERM] = REG_FIELD(0x05, 0, 3), + /* REG06 */ + [F_VREG] = REG_FIELD(0x06, 2, 7), + [F_BATLOWV] = REG_FIELD(0x06, 1, 1), + [F_VRECHG] = REG_FIELD(0x06, 0, 0), + /* REG07 */ + [F_TERM_EN] = REG_FIELD(0x07, 7, 7), + [F_STAT_DIS] = REG_FIELD(0x07, 6, 6), + [F_WD] = REG_FIELD(0x07, 4, 5), + [F_TMR_EN] = REG_FIELD(0x07, 3, 3), + [F_CHG_TMR] = REG_FIELD(0x07, 1, 2), + [F_JEITA_ISET] = REG_FIELD(0x07, 0, 0), // reserved on BQ25895 + /* REG08 */ + [F_BATCMP] = REG_FIELD(0x08, 5, 7), + [F_VCLAMP] = REG_FIELD(0x08, 2, 4), + [F_TREG] = REG_FIELD(0x08, 0, 1), + /* REG09 */ + [F_FORCE_ICO] = REG_FIELD(0x09, 7, 7), + [F_TMR2X_EN] = REG_FIELD(0x09, 6, 6), + [F_BATFET_DIS] = REG_FIELD(0x09, 5, 5), + [F_JEITA_VSET] = REG_FIELD(0x09, 4, 4), // reserved on BQ25895 + [F_BATFET_DLY] = REG_FIELD(0x09, 3, 3), + [F_BATFET_RST_EN] = REG_FIELD(0x09, 2, 2), + [F_PUMPX_UP] = REG_FIELD(0x09, 1, 1), + [F_PUMPX_DN] = REG_FIELD(0x09, 0, 0), + /* REG0A */ + [F_BOOSTV] = REG_FIELD(0x0A, 4, 7), + [F_BOOSTI] = REG_FIELD(0x0A, 0, 2), // reserved on BQ25895 + [F_PFM_OTG_DIS] = REG_FIELD(0x0A, 3, 3), // BQ25896 only + /* REG0B */ + [F_VBUS_STAT] = REG_FIELD(0x0B, 5, 7), + [F_CHG_STAT] = REG_FIELD(0x0B, 3, 4), + [F_PG_STAT] = REG_FIELD(0x0B, 2, 2), + [F_SDP_STAT] = REG_FIELD(0x0B, 1, 1), // reserved on BQ25896 + [F_VSYS_STAT] = REG_FIELD(0x0B, 0, 0), + /* REG0C */ + [F_WD_FAULT] = REG_FIELD(0x0C, 7, 7), + [F_BOOST_FAULT] = REG_FIELD(0x0C, 6, 6), + [F_CHG_FAULT] = REG_FIELD(0x0C, 4, 5), + [F_BAT_FAULT] = REG_FIELD(0x0C, 3, 3), + [F_NTC_FAULT] = REG_FIELD(0x0C, 0, 2), + /* REG0D */ + [F_FORCE_VINDPM] = REG_FIELD(0x0D, 7, 7), + [F_VINDPM] = REG_FIELD(0x0D, 0, 6), + /* REG0E */ + [F_THERM_STAT] = REG_FIELD(0x0E, 7, 7), + [F_BATV] = REG_FIELD(0x0E, 0, 6), + /* REG0F */ + [F_SYSV] = REG_FIELD(0x0F, 0, 6), + /* REG10 */ + [F_TSPCT] = REG_FIELD(0x10, 0, 6), + /* REG11 */ + [F_VBUS_GD] = REG_FIELD(0x11, 7, 7), + [F_VBUSV] = REG_FIELD(0x11, 0, 6), + /* REG12 */ + [F_ICHGR] = REG_FIELD(0x12, 0, 6), + /* REG13 */ + [F_VDPM_STAT] = REG_FIELD(0x13, 7, 7), + [F_IDPM_STAT] = REG_FIELD(0x13, 6, 6), + [F_IDPM_LIM] = REG_FIELD(0x13, 0, 5), + /* REG14 */ + [F_REG_RST] = REG_FIELD(0x14, 7, 7), + [F_ICO_OPTIMIZED] = REG_FIELD(0x14, 6, 6), + [F_PN] = REG_FIELD(0x14, 3, 5), + [F_TS_PROFILE] = REG_FIELD(0x14, 2, 2), + [F_DEV_REV] = REG_FIELD(0x14, 0, 1) +}; + +/* + * Most of the val -> idx conversions can be computed, given the minimum, + * maximum and the step between values. For the rest of conversions, we use + * lookup tables. + */ +enum bq25890_table_ids { + /* range tables */ + TBL_ICHG, + TBL_ITERM, + TBL_IILIM, + TBL_VREG, + TBL_BOOSTV, + TBL_SYSVMIN, + TBL_VBATCOMP, + TBL_RBATCOMP, + + /* lookup tables */ + TBL_TREG, + TBL_BOOSTI, +}; + +/* Thermal Regulation Threshold lookup table, in degrees Celsius */ +static const u32 bq25890_treg_tbl[] = { 60, 80, 100, 120 }; + +#define BQ25890_TREG_TBL_SIZE ARRAY_SIZE(bq25890_treg_tbl) + +/* Boost mode current limit lookup table, in uA */ +static const u32 bq25890_boosti_tbl[] = { + 500000, 700000, 1100000, 1300000, 1600000, 1800000, 2100000, 2400000 +}; + +#define BQ25890_BOOSTI_TBL_SIZE ARRAY_SIZE(bq25890_boosti_tbl) + +struct bq25890_range { + u32 min; + u32 max; + u32 step; +}; + +struct bq25890_lookup { + const u32 *tbl; + u32 size; +}; + +static const union { + struct bq25890_range rt; + struct bq25890_lookup lt; +} bq25890_tables[] = { + /* range tables */ + /* TODO: BQ25896 has max ICHG 3008 mA */ + [TBL_ICHG] = { .rt = {0, 5056000, 64000} }, /* uA */ + [TBL_ITERM] = { .rt = {64000, 1024000, 64000} }, /* uA */ + [TBL_IILIM] = { .rt = {100000, 3250000, 50000} }, /* uA */ + [TBL_VREG] = { .rt = {3840000, 4608000, 16000} }, /* uV */ + [TBL_BOOSTV] = { .rt = {4550000, 5510000, 64000} }, /* uV */ + [TBL_SYSVMIN] = { .rt = {3000000, 3700000, 100000} }, /* uV */ + [TBL_VBATCOMP] ={ .rt = {0, 224000, 32000} }, /* uV */ + [TBL_RBATCOMP] ={ .rt = {0, 140000, 20000} }, /* uOhm */ + + /* lookup tables */ + [TBL_TREG] = { .lt = {bq25890_treg_tbl, BQ25890_TREG_TBL_SIZE} }, + [TBL_BOOSTI] = { .lt = {bq25890_boosti_tbl, BQ25890_BOOSTI_TBL_SIZE} } +}; + +static int bq25890_field_read(struct bq25890_device *bq, + enum bq25890_fields field_id) +{ + int ret; + int val; + + ret = regmap_field_read(bq->rmap_fields[field_id], &val); + if (ret < 0) + return ret; + + return val; +} + +static int bq25890_field_write(struct bq25890_device *bq, + enum bq25890_fields field_id, u8 val) +{ + return regmap_field_write(bq->rmap_fields[field_id], val); +} + +static u8 bq25890_find_idx(u32 value, enum bq25890_table_ids id) +{ + u8 idx; + + if (id >= TBL_TREG) { + const u32 *tbl = bq25890_tables[id].lt.tbl; + u32 tbl_size = bq25890_tables[id].lt.size; + + for (idx = 1; idx < tbl_size && tbl[idx] <= value; idx++) + ; + } else { + const struct bq25890_range *rtbl = &bq25890_tables[id].rt; + u8 rtbl_size; + + rtbl_size = (rtbl->max - rtbl->min) / rtbl->step + 1; + + for (idx = 1; + idx < rtbl_size && (idx * rtbl->step + rtbl->min <= value); + idx++) + ; + } + + return idx - 1; +} + +static u32 bq25890_find_val(u8 idx, enum bq25890_table_ids id) +{ + const struct bq25890_range *rtbl; + + /* lookup table? */ + if (id >= TBL_TREG) + return bq25890_tables[id].lt.tbl[idx]; + + /* range table */ + rtbl = &bq25890_tables[id].rt; + + return (rtbl->min + idx * rtbl->step); +} + +enum bq25890_status { + STATUS_NOT_CHARGING, + STATUS_PRE_CHARGING, + STATUS_FAST_CHARGING, + STATUS_TERMINATION_DONE, +}; + +enum bq25890_chrg_fault { + CHRG_FAULT_NORMAL, + CHRG_FAULT_INPUT, + CHRG_FAULT_THERMAL_SHUTDOWN, + CHRG_FAULT_TIMER_EXPIRED, +}; + +static bool bq25890_is_adc_property(enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_CURRENT_NOW: + return true; + + default: + return false; + } +} + +static irqreturn_t __bq25890_handle_irq(struct bq25890_device *bq); + +static int bq25890_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq25890_device *bq = power_supply_get_drvdata(psy); + struct bq25890_state state; + bool do_adc_conv; + int ret; + + mutex_lock(&bq->lock); + /* update state in case we lost an interrupt */ + __bq25890_handle_irq(bq); + state = bq->state; + do_adc_conv = !state.online && bq25890_is_adc_property(psp); + if (do_adc_conv) + bq25890_field_write(bq, F_CONV_START, 1); + mutex_unlock(&bq->lock); + + if (do_adc_conv) + regmap_field_read_poll_timeout(bq->rmap_fields[F_CONV_START], + ret, !ret, 25000, 1000000); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (!state.online) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (state.chrg_status == STATUS_NOT_CHARGING) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (state.chrg_status == STATUS_PRE_CHARGING || + state.chrg_status == STATUS_FAST_CHARGING) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (state.chrg_status == STATUS_TERMINATION_DONE) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + if (!state.online || state.chrg_status == STATUS_NOT_CHARGING || + state.chrg_status == STATUS_TERMINATION_DONE) + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + else if (state.chrg_status == STATUS_PRE_CHARGING) + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + else if (state.chrg_status == STATUS_FAST_CHARGING) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + else /* unreachable */ + val->intval = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ25890_MANUFACTURER; + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bq25890_chip_name[bq->chip_version]; + break; + + case POWER_SUPPLY_PROP_ONLINE: + val->intval = state.online; + break; + + case POWER_SUPPLY_PROP_HEALTH: + if (!state.chrg_fault && !state.bat_fault && !state.boost_fault) + val->intval = POWER_SUPPLY_HEALTH_GOOD; + else if (state.bat_fault) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (state.chrg_fault == CHRG_FAULT_TIMER_EXPIRED) + val->intval = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + else if (state.chrg_fault == CHRG_FAULT_THERMAL_SHUTDOWN) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = bq25890_find_val(bq->init_data.ichg, TBL_ICHG); + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + if (!state.online) { + val->intval = 0; + break; + } + + ret = bq25890_field_read(bq, F_BATV); /* read measured value */ + if (ret < 0) + return ret; + + /* converted_val = 2.304V + ADC_val * 20mV (table 10.3.15) */ + val->intval = 2304000 + ret * 20000; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + val->intval = bq25890_find_val(bq->init_data.vreg, TBL_VREG); + break; + + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + val->intval = bq25890_find_val(bq->init_data.iprechg, TBL_ITERM); + break; + + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + val->intval = bq25890_find_val(bq->init_data.iterm, TBL_ITERM); + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq25890_field_read(bq, F_IILIM); + if (ret < 0) + return ret; + + val->intval = bq25890_find_val(ret, TBL_IILIM); + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = bq25890_field_read(bq, F_SYSV); /* read measured value */ + if (ret < 0) + return ret; + + /* converted_val = 2.304V + ADC_val * 20mV (table 10.3.15) */ + val->intval = 2304000 + ret * 20000; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = bq25890_field_read(bq, F_ICHGR); /* read measured value */ + if (ret < 0) + return ret; + + /* converted_val = ADC_val * 50mA (table 10.3.19) */ + val->intval = ret * -50000; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int bq25890_get_chip_state(struct bq25890_device *bq, + struct bq25890_state *state) +{ + int i, ret; + + struct { + enum bq25890_fields id; + u8 *data; + } state_fields[] = { + {F_CHG_STAT, &state->chrg_status}, + {F_PG_STAT, &state->online}, + {F_VSYS_STAT, &state->vsys_status}, + {F_BOOST_FAULT, &state->boost_fault}, + {F_BAT_FAULT, &state->bat_fault}, + {F_CHG_FAULT, &state->chrg_fault} + }; + + for (i = 0; i < ARRAY_SIZE(state_fields); i++) { + ret = bq25890_field_read(bq, state_fields[i].id); + if (ret < 0) + return ret; + + *state_fields[i].data = ret; + } + + dev_dbg(bq->dev, "S:CHG/PG/VSYS=%d/%d/%d, F:CHG/BOOST/BAT=%d/%d/%d\n", + state->chrg_status, state->online, state->vsys_status, + state->chrg_fault, state->boost_fault, state->bat_fault); + + return 0; +} + +static irqreturn_t __bq25890_handle_irq(struct bq25890_device *bq) +{ + struct bq25890_state new_state; + int ret; + + ret = bq25890_get_chip_state(bq, &new_state); + if (ret < 0) + return IRQ_NONE; + + if (!memcmp(&bq->state, &new_state, sizeof(new_state))) + return IRQ_NONE; + + if (!new_state.online && bq->state.online) { /* power removed */ + /* disable ADC */ + ret = bq25890_field_write(bq, F_CONV_RATE, 0); + if (ret < 0) + goto error; + } else if (new_state.online && !bq->state.online) { /* power inserted */ + /* enable ADC, to have control of charge current/voltage */ + ret = bq25890_field_write(bq, F_CONV_RATE, 1); + if (ret < 0) + goto error; + } + + bq->state = new_state; + power_supply_changed(bq->charger); + + return IRQ_HANDLED; +error: + dev_err(bq->dev, "Error communicating with the chip: %pe\n", + ERR_PTR(ret)); + return IRQ_HANDLED; +} + +static irqreturn_t bq25890_irq_handler_thread(int irq, void *private) +{ + struct bq25890_device *bq = private; + irqreturn_t ret; + + mutex_lock(&bq->lock); + ret = __bq25890_handle_irq(bq); + mutex_unlock(&bq->lock); + + return ret; +} + +static int bq25890_chip_reset(struct bq25890_device *bq) +{ + int ret; + int rst_check_counter = 10; + + ret = bq25890_field_write(bq, F_REG_RST, 1); + if (ret < 0) + return ret; + + do { + ret = bq25890_field_read(bq, F_REG_RST); + if (ret < 0) + return ret; + + usleep_range(5, 10); + } while (ret == 1 && --rst_check_counter); + + if (!rst_check_counter) + return -ETIMEDOUT; + + return 0; +} + +static int bq25890_hw_init(struct bq25890_device *bq) +{ + int ret; + int i; + + const struct { + enum bq25890_fields id; + u32 value; + } init_data[] = { + {F_ICHG, bq->init_data.ichg}, + {F_VREG, bq->init_data.vreg}, + {F_ITERM, bq->init_data.iterm}, + {F_IPRECHG, bq->init_data.iprechg}, + {F_SYSVMIN, bq->init_data.sysvmin}, + {F_BOOSTV, bq->init_data.boostv}, + {F_BOOSTI, bq->init_data.boosti}, + {F_BOOSTF, bq->init_data.boostf}, + {F_EN_ILIM, bq->init_data.ilim_en}, + {F_TREG, bq->init_data.treg}, + {F_BATCMP, bq->init_data.rbatcomp}, + {F_VCLAMP, bq->init_data.vclamp}, + }; + + ret = bq25890_chip_reset(bq); + if (ret < 0) { + dev_dbg(bq->dev, "Reset failed %d\n", ret); + return ret; + } + + /* disable watchdog */ + ret = bq25890_field_write(bq, F_WD, 0); + if (ret < 0) { + dev_dbg(bq->dev, "Disabling watchdog failed %d\n", ret); + return ret; + } + + /* initialize currents/voltages and other parameters */ + for (i = 0; i < ARRAY_SIZE(init_data); i++) { + ret = bq25890_field_write(bq, init_data[i].id, + init_data[i].value); + if (ret < 0) { + dev_dbg(bq->dev, "Writing init data failed %d\n", ret); + return ret; + } + } + + /* Configure ADC for continuous conversions when charging */ + ret = bq25890_field_write(bq, F_CONV_RATE, !!bq->state.online); + if (ret < 0) { + dev_dbg(bq->dev, "Config ADC failed %d\n", ret); + return ret; + } + + ret = bq25890_get_chip_state(bq, &bq->state); + if (ret < 0) { + dev_dbg(bq->dev, "Get state failed %d\n", ret); + return ret; + } + + return 0; +} + +static const enum power_supply_property bq25890_power_supply_props[] = { + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, + POWER_SUPPLY_PROP_PRECHARGE_CURRENT, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static char *bq25890_charger_supplied_to[] = { + "main-battery", +}; + +static const struct power_supply_desc bq25890_power_supply_desc = { + .name = "bq25890-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = bq25890_power_supply_props, + .num_properties = ARRAY_SIZE(bq25890_power_supply_props), + .get_property = bq25890_power_supply_get_property, +}; + +static int bq25890_power_supply_init(struct bq25890_device *bq) +{ + struct power_supply_config psy_cfg = { .drv_data = bq, }; + + psy_cfg.supplied_to = bq25890_charger_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(bq25890_charger_supplied_to); + + bq->charger = power_supply_register(bq->dev, &bq25890_power_supply_desc, + &psy_cfg); + + return PTR_ERR_OR_ZERO(bq->charger); +} + +static void bq25890_usb_work(struct work_struct *data) +{ + int ret; + struct bq25890_device *bq = + container_of(data, struct bq25890_device, usb_work); + + switch (bq->usb_event) { + case USB_EVENT_ID: + /* Enable boost mode */ + ret = bq25890_field_write(bq, F_OTG_CFG, 1); + if (ret < 0) + goto error; + break; + + case USB_EVENT_NONE: + /* Disable boost mode */ + ret = bq25890_field_write(bq, F_OTG_CFG, 0); + if (ret < 0) + goto error; + + power_supply_changed(bq->charger); + break; + } + + return; + +error: + dev_err(bq->dev, "Error switching to boost/charger mode.\n"); +} + +static int bq25890_usb_notifier(struct notifier_block *nb, unsigned long val, + void *priv) +{ + struct bq25890_device *bq = + container_of(nb, struct bq25890_device, usb_nb); + + bq->usb_event = val; + queue_work(system_power_efficient_wq, &bq->usb_work); + + return NOTIFY_OK; +} + +static int bq25890_get_chip_version(struct bq25890_device *bq) +{ + int id, rev; + + id = bq25890_field_read(bq, F_PN); + if (id < 0) { + dev_err(bq->dev, "Cannot read chip ID.\n"); + return id; + } + + rev = bq25890_field_read(bq, F_DEV_REV); + if (rev < 0) { + dev_err(bq->dev, "Cannot read chip revision.\n"); + return rev; + } + + switch (id) { + case BQ25890_ID: + bq->chip_version = BQ25890; + break; + + /* BQ25892 and BQ25896 share same ID 0 */ + case BQ25896_ID: + switch (rev) { + case 2: + bq->chip_version = BQ25896; + break; + case 1: + bq->chip_version = BQ25892; + break; + default: + dev_err(bq->dev, + "Unknown device revision %d, assume BQ25892\n", + rev); + bq->chip_version = BQ25892; + } + break; + + case BQ25895_ID: + bq->chip_version = BQ25895; + break; + + default: + dev_err(bq->dev, "Unknown chip ID %d\n", id); + return -ENODEV; + } + + return 0; +} + +static int bq25890_irq_probe(struct bq25890_device *bq) +{ + struct gpio_desc *irq; + + irq = devm_gpiod_get(bq->dev, BQ25890_IRQ_PIN, GPIOD_IN); + if (IS_ERR(irq)) { + dev_err(bq->dev, "Could not probe irq pin.\n"); + return PTR_ERR(irq); + } + + return gpiod_to_irq(irq); +} + +static int bq25890_fw_read_u32_props(struct bq25890_device *bq) +{ + int ret; + u32 property; + int i; + struct bq25890_init_data *init = &bq->init_data; + struct { + char *name; + bool optional; + enum bq25890_table_ids tbl_id; + u8 *conv_data; /* holds converted value from given property */ + } props[] = { + /* required properties */ + {"ti,charge-current", false, TBL_ICHG, &init->ichg}, + {"ti,battery-regulation-voltage", false, TBL_VREG, &init->vreg}, + {"ti,termination-current", false, TBL_ITERM, &init->iterm}, + {"ti,precharge-current", false, TBL_ITERM, &init->iprechg}, + {"ti,minimum-sys-voltage", false, TBL_SYSVMIN, &init->sysvmin}, + {"ti,boost-voltage", false, TBL_BOOSTV, &init->boostv}, + {"ti,boost-max-current", false, TBL_BOOSTI, &init->boosti}, + + /* optional properties */ + {"ti,thermal-regulation-threshold", true, TBL_TREG, &init->treg}, + {"ti,ibatcomp-micro-ohms", true, TBL_RBATCOMP, &init->rbatcomp}, + {"ti,ibatcomp-clamp-microvolt", true, TBL_VBATCOMP, &init->vclamp}, + }; + + /* initialize data for optional properties */ + init->treg = 3; /* 120 degrees Celsius */ + init->rbatcomp = init->vclamp = 0; /* IBAT compensation disabled */ + + for (i = 0; i < ARRAY_SIZE(props); i++) { + ret = device_property_read_u32(bq->dev, props[i].name, + &property); + if (ret < 0) { + if (props[i].optional) + continue; + + dev_err(bq->dev, "Unable to read property %d %s\n", ret, + props[i].name); + + return ret; + } + + *props[i].conv_data = bq25890_find_idx(property, + props[i].tbl_id); + } + + return 0; +} + +static int bq25890_fw_probe(struct bq25890_device *bq) +{ + int ret; + struct bq25890_init_data *init = &bq->init_data; + + ret = bq25890_fw_read_u32_props(bq); + if (ret < 0) + return ret; + + init->ilim_en = device_property_read_bool(bq->dev, "ti,use-ilim-pin"); + init->boostf = device_property_read_bool(bq->dev, "ti,boost-low-freq"); + + return 0; +} + +static int bq25890_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct bq25890_device *bq; + int ret; + int i; + + bq = devm_kzalloc(dev, sizeof(*bq), GFP_KERNEL); + if (!bq) + return -ENOMEM; + + bq->client = client; + bq->dev = dev; + + mutex_init(&bq->lock); + + bq->rmap = devm_regmap_init_i2c(client, &bq25890_regmap_config); + if (IS_ERR(bq->rmap)) { + dev_err(dev, "failed to allocate register map\n"); + return PTR_ERR(bq->rmap); + } + + for (i = 0; i < ARRAY_SIZE(bq25890_reg_fields); i++) { + const struct reg_field *reg_fields = bq25890_reg_fields; + + bq->rmap_fields[i] = devm_regmap_field_alloc(dev, bq->rmap, + reg_fields[i]); + if (IS_ERR(bq->rmap_fields[i])) { + dev_err(dev, "cannot allocate regmap field\n"); + return PTR_ERR(bq->rmap_fields[i]); + } + } + + i2c_set_clientdata(client, bq); + + ret = bq25890_get_chip_version(bq); + if (ret) { + dev_err(dev, "Cannot read chip ID or unknown chip.\n"); + return ret; + } + + if (!dev->platform_data) { + ret = bq25890_fw_probe(bq); + if (ret < 0) { + dev_err(dev, "Cannot read device properties.\n"); + return ret; + } + } else { + return -ENODEV; + } + + ret = bq25890_hw_init(bq); + if (ret < 0) { + dev_err(dev, "Cannot initialize the chip.\n"); + return ret; + } + + if (client->irq <= 0) + client->irq = bq25890_irq_probe(bq); + + if (client->irq < 0) { + dev_err(dev, "No irq resource found.\n"); + return client->irq; + } + + /* OTG reporting */ + bq->usb_phy = devm_usb_get_phy(dev, USB_PHY_TYPE_USB2); + if (!IS_ERR_OR_NULL(bq->usb_phy)) { + INIT_WORK(&bq->usb_work, bq25890_usb_work); + bq->usb_nb.notifier_call = bq25890_usb_notifier; + usb_register_notifier(bq->usb_phy, &bq->usb_nb); + } + + ret = devm_request_threaded_irq(dev, client->irq, NULL, + bq25890_irq_handler_thread, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + BQ25890_IRQ_PIN, bq); + if (ret) + goto irq_fail; + + ret = bq25890_power_supply_init(bq); + if (ret < 0) { + dev_err(dev, "Failed to register power supply\n"); + goto irq_fail; + } + + return 0; + +irq_fail: + if (!IS_ERR_OR_NULL(bq->usb_phy)) + usb_unregister_notifier(bq->usb_phy, &bq->usb_nb); + + return ret; +} + +static int bq25890_remove(struct i2c_client *client) +{ + struct bq25890_device *bq = i2c_get_clientdata(client); + + power_supply_unregister(bq->charger); + + if (!IS_ERR_OR_NULL(bq->usb_phy)) + usb_unregister_notifier(bq->usb_phy, &bq->usb_nb); + + /* reset all registers to default values */ + bq25890_chip_reset(bq); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int bq25890_suspend(struct device *dev) +{ + struct bq25890_device *bq = dev_get_drvdata(dev); + + /* + * If charger is removed, while in suspend, make sure ADC is diabled + * since it consumes slightly more power. + */ + return bq25890_field_write(bq, F_CONV_RATE, 0); +} + +static int bq25890_resume(struct device *dev) +{ + int ret; + struct bq25890_device *bq = dev_get_drvdata(dev); + + mutex_lock(&bq->lock); + + ret = bq25890_get_chip_state(bq, &bq->state); + if (ret < 0) + goto unlock; + + /* Re-enable ADC only if charger is plugged in. */ + if (bq->state.online) { + ret = bq25890_field_write(bq, F_CONV_RATE, 1); + if (ret < 0) + goto unlock; + } + + /* signal userspace, maybe state changed while suspended */ + power_supply_changed(bq->charger); + +unlock: + mutex_unlock(&bq->lock); + + return ret; +} +#endif + +static const struct dev_pm_ops bq25890_pm = { + SET_SYSTEM_SLEEP_PM_OPS(bq25890_suspend, bq25890_resume) +}; + +static const struct i2c_device_id bq25890_i2c_ids[] = { + { "bq25890", 0 }, + { "bq25892", 0 }, + { "bq25895", 0 }, + { "bq25896", 0 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq25890_i2c_ids); + +static const struct of_device_id bq25890_of_match[] = { + { .compatible = "ti,bq25890", }, + { .compatible = "ti,bq25892", }, + { .compatible = "ti,bq25895", }, + { .compatible = "ti,bq25896", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bq25890_of_match); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id bq25890_acpi_match[] = { + {"BQ258900", 0}, + {}, +}; +MODULE_DEVICE_TABLE(acpi, bq25890_acpi_match); +#endif + +static struct i2c_driver bq25890_driver = { + .driver = { + .name = "bq25890-charger", + .of_match_table = of_match_ptr(bq25890_of_match), + .acpi_match_table = ACPI_PTR(bq25890_acpi_match), + .pm = &bq25890_pm, + }, + .probe = bq25890_probe, + .remove = bq25890_remove, + .id_table = bq25890_i2c_ids, +}; +module_i2c_driver(bq25890_driver); + +MODULE_AUTHOR("Laurentiu Palcu <laurentiu.palcu@intel.com>"); +MODULE_DESCRIPTION("bq25890 charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bq25980_charger.c b/drivers/power/supply/bq25980_charger.c new file mode 100644 index 000000000..b94ecf814 --- /dev/null +++ b/drivers/power/supply/bq25980_charger.c @@ -0,0 +1,1298 @@ +// SPDX-License-Identifier: GPL-2.0 +// BQ25980 Battery Charger Driver +// Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com/ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/gpio/consumer.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/types.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/moduleparam.h> +#include <linux/slab.h> + +#include "bq25980_charger.h" + +struct bq25980_state { + bool dischg; + bool ovp; + bool ocp; + bool wdt; + bool tflt; + bool online; + bool ce; + bool hiz; + bool bypass; + + u32 vbat_adc; + u32 vsys_adc; + u32 ibat_adc; +}; + +enum bq25980_id { + BQ25980, + BQ25975, + BQ25960, +}; + +struct bq25980_chip_info { + + int model_id; + + const struct regmap_config *regmap_config; + + int busocp_def; + int busocp_sc_max; + int busocp_byp_max; + int busocp_sc_min; + int busocp_byp_min; + + int busovp_sc_def; + int busovp_byp_def; + int busovp_sc_step; + + int busovp_sc_offset; + int busovp_byp_step; + int busovp_byp_offset; + int busovp_sc_min; + int busovp_sc_max; + int busovp_byp_min; + int busovp_byp_max; + + int batovp_def; + int batovp_max; + int batovp_min; + int batovp_step; + int batovp_offset; + + int batocp_def; + int batocp_max; +}; + +struct bq25980_init_data { + u32 ichg; + u32 bypass_ilim; + u32 sc_ilim; + u32 vreg; + u32 iterm; + u32 iprechg; + u32 bypass_vlim; + u32 sc_vlim; + u32 ichg_max; + u32 vreg_max; +}; + +struct bq25980_device { + struct i2c_client *client; + struct device *dev; + struct power_supply *charger; + struct power_supply *battery; + struct mutex lock; + struct regmap *regmap; + + char model_name[I2C_NAME_SIZE]; + + struct bq25980_init_data init_data; + const struct bq25980_chip_info *chip_info; + struct bq25980_state state; + int watchdog_timer; +}; + +static struct reg_default bq25980_reg_defs[] = { + {BQ25980_BATOVP, 0x5A}, + {BQ25980_BATOVP_ALM, 0x46}, + {BQ25980_BATOCP, 0x51}, + {BQ25980_BATOCP_ALM, 0x50}, + {BQ25980_BATUCP_ALM, 0x28}, + {BQ25980_CHRGR_CTRL_1, 0x0}, + {BQ25980_BUSOVP, 0x26}, + {BQ25980_BUSOVP_ALM, 0x22}, + {BQ25980_BUSOCP, 0xD}, + {BQ25980_BUSOCP_ALM, 0xC}, + {BQ25980_TEMP_CONTROL, 0x30}, + {BQ25980_TDIE_ALM, 0xC8}, + {BQ25980_TSBUS_FLT, 0x15}, + {BQ25980_TSBAT_FLG, 0x15}, + {BQ25980_VAC_CONTROL, 0x0}, + {BQ25980_CHRGR_CTRL_2, 0x0}, + {BQ25980_CHRGR_CTRL_3, 0x20}, + {BQ25980_CHRGR_CTRL_4, 0x1D}, + {BQ25980_CHRGR_CTRL_5, 0x18}, + {BQ25980_STAT1, 0x0}, + {BQ25980_STAT2, 0x0}, + {BQ25980_STAT3, 0x0}, + {BQ25980_STAT4, 0x0}, + {BQ25980_STAT5, 0x0}, + {BQ25980_FLAG1, 0x0}, + {BQ25980_FLAG2, 0x0}, + {BQ25980_FLAG3, 0x0}, + {BQ25980_FLAG4, 0x0}, + {BQ25980_FLAG5, 0x0}, + {BQ25980_MASK1, 0x0}, + {BQ25980_MASK2, 0x0}, + {BQ25980_MASK3, 0x0}, + {BQ25980_MASK4, 0x0}, + {BQ25980_MASK5, 0x0}, + {BQ25980_DEVICE_INFO, 0x8}, + {BQ25980_ADC_CONTROL1, 0x0}, + {BQ25980_ADC_CONTROL2, 0x0}, + {BQ25980_IBUS_ADC_LSB, 0x0}, + {BQ25980_IBUS_ADC_MSB, 0x0}, + {BQ25980_VBUS_ADC_LSB, 0x0}, + {BQ25980_VBUS_ADC_MSB, 0x0}, + {BQ25980_VAC1_ADC_LSB, 0x0}, + {BQ25980_VAC2_ADC_LSB, 0x0}, + {BQ25980_VOUT_ADC_LSB, 0x0}, + {BQ25980_VBAT_ADC_LSB, 0x0}, + {BQ25980_IBAT_ADC_MSB, 0x0}, + {BQ25980_IBAT_ADC_LSB, 0x0}, + {BQ25980_TSBUS_ADC_LSB, 0x0}, + {BQ25980_TSBAT_ADC_LSB, 0x0}, + {BQ25980_TDIE_ADC_LSB, 0x0}, + {BQ25980_DEGLITCH_TIME, 0x0}, + {BQ25980_CHRGR_CTRL_6, 0x0}, +}; + +static struct reg_default bq25975_reg_defs[] = { + {BQ25980_BATOVP, 0x5A}, + {BQ25980_BATOVP_ALM, 0x46}, + {BQ25980_BATOCP, 0x51}, + {BQ25980_BATOCP_ALM, 0x50}, + {BQ25980_BATUCP_ALM, 0x28}, + {BQ25980_CHRGR_CTRL_1, 0x0}, + {BQ25980_BUSOVP, 0x26}, + {BQ25980_BUSOVP_ALM, 0x22}, + {BQ25980_BUSOCP, 0xD}, + {BQ25980_BUSOCP_ALM, 0xC}, + {BQ25980_TEMP_CONTROL, 0x30}, + {BQ25980_TDIE_ALM, 0xC8}, + {BQ25980_TSBUS_FLT, 0x15}, + {BQ25980_TSBAT_FLG, 0x15}, + {BQ25980_VAC_CONTROL, 0x0}, + {BQ25980_CHRGR_CTRL_2, 0x0}, + {BQ25980_CHRGR_CTRL_3, 0x20}, + {BQ25980_CHRGR_CTRL_4, 0x1D}, + {BQ25980_CHRGR_CTRL_5, 0x18}, + {BQ25980_STAT1, 0x0}, + {BQ25980_STAT2, 0x0}, + {BQ25980_STAT3, 0x0}, + {BQ25980_STAT4, 0x0}, + {BQ25980_STAT5, 0x0}, + {BQ25980_FLAG1, 0x0}, + {BQ25980_FLAG2, 0x0}, + {BQ25980_FLAG3, 0x0}, + {BQ25980_FLAG4, 0x0}, + {BQ25980_FLAG5, 0x0}, + {BQ25980_MASK1, 0x0}, + {BQ25980_MASK2, 0x0}, + {BQ25980_MASK3, 0x0}, + {BQ25980_MASK4, 0x0}, + {BQ25980_MASK5, 0x0}, + {BQ25980_DEVICE_INFO, 0x8}, + {BQ25980_ADC_CONTROL1, 0x0}, + {BQ25980_ADC_CONTROL2, 0x0}, + {BQ25980_IBUS_ADC_LSB, 0x0}, + {BQ25980_IBUS_ADC_MSB, 0x0}, + {BQ25980_VBUS_ADC_LSB, 0x0}, + {BQ25980_VBUS_ADC_MSB, 0x0}, + {BQ25980_VAC1_ADC_LSB, 0x0}, + {BQ25980_VAC2_ADC_LSB, 0x0}, + {BQ25980_VOUT_ADC_LSB, 0x0}, + {BQ25980_VBAT_ADC_LSB, 0x0}, + {BQ25980_IBAT_ADC_MSB, 0x0}, + {BQ25980_IBAT_ADC_LSB, 0x0}, + {BQ25980_TSBUS_ADC_LSB, 0x0}, + {BQ25980_TSBAT_ADC_LSB, 0x0}, + {BQ25980_TDIE_ADC_LSB, 0x0}, + {BQ25980_DEGLITCH_TIME, 0x0}, + {BQ25980_CHRGR_CTRL_6, 0x0}, +}; + +static struct reg_default bq25960_reg_defs[] = { + {BQ25980_BATOVP, 0x5A}, + {BQ25980_BATOVP_ALM, 0x46}, + {BQ25980_BATOCP, 0x51}, + {BQ25980_BATOCP_ALM, 0x50}, + {BQ25980_BATUCP_ALM, 0x28}, + {BQ25980_CHRGR_CTRL_1, 0x0}, + {BQ25980_BUSOVP, 0x26}, + {BQ25980_BUSOVP_ALM, 0x22}, + {BQ25980_BUSOCP, 0xD}, + {BQ25980_BUSOCP_ALM, 0xC}, + {BQ25980_TEMP_CONTROL, 0x30}, + {BQ25980_TDIE_ALM, 0xC8}, + {BQ25980_TSBUS_FLT, 0x15}, + {BQ25980_TSBAT_FLG, 0x15}, + {BQ25980_VAC_CONTROL, 0x0}, + {BQ25980_CHRGR_CTRL_2, 0x0}, + {BQ25980_CHRGR_CTRL_3, 0x20}, + {BQ25980_CHRGR_CTRL_4, 0x1D}, + {BQ25980_CHRGR_CTRL_5, 0x18}, + {BQ25980_STAT1, 0x0}, + {BQ25980_STAT2, 0x0}, + {BQ25980_STAT3, 0x0}, + {BQ25980_STAT4, 0x0}, + {BQ25980_STAT5, 0x0}, + {BQ25980_FLAG1, 0x0}, + {BQ25980_FLAG2, 0x0}, + {BQ25980_FLAG3, 0x0}, + {BQ25980_FLAG4, 0x0}, + {BQ25980_FLAG5, 0x0}, + {BQ25980_MASK1, 0x0}, + {BQ25980_MASK2, 0x0}, + {BQ25980_MASK3, 0x0}, + {BQ25980_MASK4, 0x0}, + {BQ25980_MASK5, 0x0}, + {BQ25980_DEVICE_INFO, 0x8}, + {BQ25980_ADC_CONTROL1, 0x0}, + {BQ25980_ADC_CONTROL2, 0x0}, + {BQ25980_IBUS_ADC_LSB, 0x0}, + {BQ25980_IBUS_ADC_MSB, 0x0}, + {BQ25980_VBUS_ADC_LSB, 0x0}, + {BQ25980_VBUS_ADC_MSB, 0x0}, + {BQ25980_VAC1_ADC_LSB, 0x0}, + {BQ25980_VAC2_ADC_LSB, 0x0}, + {BQ25980_VOUT_ADC_LSB, 0x0}, + {BQ25980_VBAT_ADC_LSB, 0x0}, + {BQ25980_IBAT_ADC_MSB, 0x0}, + {BQ25980_IBAT_ADC_LSB, 0x0}, + {BQ25980_TSBUS_ADC_LSB, 0x0}, + {BQ25980_TSBAT_ADC_LSB, 0x0}, + {BQ25980_TDIE_ADC_LSB, 0x0}, + {BQ25980_DEGLITCH_TIME, 0x0}, + {BQ25980_CHRGR_CTRL_6, 0x0}, +}; + +static int bq25980_watchdog_time[BQ25980_NUM_WD_VAL] = {5000, 10000, 50000, + 300000}; + +static int bq25980_get_input_curr_lim(struct bq25980_device *bq) +{ + unsigned int busocp_reg_code; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_BUSOCP, &busocp_reg_code); + if (ret) + return ret; + + return (busocp_reg_code * BQ25980_BUSOCP_STEP_uA) + BQ25980_BUSOCP_OFFSET_uA; +} + +static int bq25980_set_hiz(struct bq25980_device *bq, int setting) +{ + return regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_EN_HIZ, setting); +} + +static int bq25980_set_input_curr_lim(struct bq25980_device *bq, int busocp) +{ + unsigned int busocp_reg_code; + int ret; + + if (!busocp) + return bq25980_set_hiz(bq, BQ25980_ENABLE_HIZ); + + bq25980_set_hiz(bq, BQ25980_DISABLE_HIZ); + + if (busocp < BQ25980_BUSOCP_MIN_uA) + busocp = BQ25980_BUSOCP_MIN_uA; + + if (bq->state.bypass) + busocp = min(busocp, bq->chip_info->busocp_sc_max); + else + busocp = min(busocp, bq->chip_info->busocp_byp_max); + + busocp_reg_code = (busocp - BQ25980_BUSOCP_OFFSET_uA) + / BQ25980_BUSOCP_STEP_uA; + + ret = regmap_write(bq->regmap, BQ25980_BUSOCP, busocp_reg_code); + if (ret) + return ret; + + return regmap_write(bq->regmap, BQ25980_BUSOCP_ALM, busocp_reg_code); +} + +static int bq25980_get_input_volt_lim(struct bq25980_device *bq) +{ + unsigned int busovp_reg_code; + unsigned int busovp_offset; + unsigned int busovp_step; + int ret; + + if (bq->state.bypass) { + busovp_step = bq->chip_info->busovp_byp_step; + busovp_offset = bq->chip_info->busovp_byp_offset; + } else { + busovp_step = bq->chip_info->busovp_sc_step; + busovp_offset = bq->chip_info->busovp_sc_offset; + } + + ret = regmap_read(bq->regmap, BQ25980_BUSOVP, &busovp_reg_code); + if (ret) + return ret; + + return (busovp_reg_code * busovp_step) + busovp_offset; +} + +static int bq25980_set_input_volt_lim(struct bq25980_device *bq, int busovp) +{ + unsigned int busovp_reg_code; + unsigned int busovp_step; + unsigned int busovp_offset; + int ret; + + if (bq->state.bypass) { + busovp_step = bq->chip_info->busovp_byp_step; + busovp_offset = bq->chip_info->busovp_byp_offset; + if (busovp > bq->chip_info->busovp_byp_max) + busovp = bq->chip_info->busovp_byp_max; + else if (busovp < bq->chip_info->busovp_byp_min) + busovp = bq->chip_info->busovp_byp_min; + } else { + busovp_step = bq->chip_info->busovp_sc_step; + busovp_offset = bq->chip_info->busovp_sc_offset; + if (busovp > bq->chip_info->busovp_sc_max) + busovp = bq->chip_info->busovp_sc_max; + else if (busovp < bq->chip_info->busovp_sc_min) + busovp = bq->chip_info->busovp_sc_min; + } + + busovp_reg_code = (busovp - busovp_offset) / busovp_step; + + ret = regmap_write(bq->regmap, BQ25980_BUSOVP, busovp_reg_code); + if (ret) + return ret; + + return regmap_write(bq->regmap, BQ25980_BUSOVP_ALM, busovp_reg_code); +} + +static int bq25980_get_const_charge_curr(struct bq25980_device *bq) +{ + unsigned int batocp_reg_code; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_BATOCP, &batocp_reg_code); + if (ret) + return ret; + + return (batocp_reg_code & BQ25980_BATOCP_MASK) * + BQ25980_BATOCP_STEP_uA; +} + +static int bq25980_set_const_charge_curr(struct bq25980_device *bq, int batocp) +{ + unsigned int batocp_reg_code; + int ret; + + batocp = max(batocp, BQ25980_BATOCP_MIN_uA); + batocp = min(batocp, bq->chip_info->batocp_max); + + batocp_reg_code = batocp / BQ25980_BATOCP_STEP_uA; + + ret = regmap_update_bits(bq->regmap, BQ25980_BATOCP, + BQ25980_BATOCP_MASK, batocp_reg_code); + if (ret) + return ret; + + return regmap_update_bits(bq->regmap, BQ25980_BATOCP_ALM, + BQ25980_BATOCP_MASK, batocp_reg_code); +} + +static int bq25980_get_const_charge_volt(struct bq25980_device *bq) +{ + unsigned int batovp_reg_code; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_BATOVP, &batovp_reg_code); + if (ret) + return ret; + + return ((batovp_reg_code * bq->chip_info->batovp_step) + + bq->chip_info->batovp_offset); +} + +static int bq25980_set_const_charge_volt(struct bq25980_device *bq, int batovp) +{ + unsigned int batovp_reg_code; + int ret; + + if (batovp < bq->chip_info->batovp_min) + batovp = bq->chip_info->batovp_min; + + if (batovp > bq->chip_info->batovp_max) + batovp = bq->chip_info->batovp_max; + + batovp_reg_code = (batovp - bq->chip_info->batovp_offset) / + bq->chip_info->batovp_step; + + ret = regmap_write(bq->regmap, BQ25980_BATOVP, batovp_reg_code); + if (ret) + return ret; + + return regmap_write(bq->regmap, BQ25980_BATOVP_ALM, batovp_reg_code); +} + +static int bq25980_set_bypass(struct bq25980_device *bq, bool en_bypass) +{ + int ret; + + if (en_bypass) + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_EN_BYPASS, BQ25980_EN_BYPASS); + else + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_EN_BYPASS, en_bypass); + if (ret) + return ret; + + bq->state.bypass = en_bypass; + + return bq->state.bypass; +} + +static int bq25980_set_chg_en(struct bq25980_device *bq, bool en_chg) +{ + int ret; + + if (en_chg) + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_CHG_EN, BQ25980_CHG_EN); + else + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_CHG_EN, en_chg); + if (ret) + return ret; + + bq->state.ce = en_chg; + + return 0; +} + +static int bq25980_get_adc_ibus(struct bq25980_device *bq) +{ + int ibus_adc_lsb, ibus_adc_msb; + u16 ibus_adc; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_IBUS_ADC_MSB, &ibus_adc_msb); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_IBUS_ADC_LSB, &ibus_adc_lsb); + if (ret) + return ret; + + ibus_adc = (ibus_adc_msb << 8) | ibus_adc_lsb; + + if (ibus_adc_msb & BQ25980_ADC_POLARITY_BIT) + return ((ibus_adc ^ 0xffff) + 1) * BQ25980_ADC_CURR_STEP_uA; + + return ibus_adc * BQ25980_ADC_CURR_STEP_uA; +} + +static int bq25980_get_adc_vbus(struct bq25980_device *bq) +{ + int vbus_adc_lsb, vbus_adc_msb; + u16 vbus_adc; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_VBUS_ADC_MSB, &vbus_adc_msb); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_VBUS_ADC_LSB, &vbus_adc_lsb); + if (ret) + return ret; + + vbus_adc = (vbus_adc_msb << 8) | vbus_adc_lsb; + + return vbus_adc * BQ25980_ADC_VOLT_STEP_uV; +} + +static int bq25980_get_ibat_adc(struct bq25980_device *bq) +{ + int ret; + int ibat_adc_lsb, ibat_adc_msb; + int ibat_adc; + + ret = regmap_read(bq->regmap, BQ25980_IBAT_ADC_MSB, &ibat_adc_msb); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_IBAT_ADC_LSB, &ibat_adc_lsb); + if (ret) + return ret; + + ibat_adc = (ibat_adc_msb << 8) | ibat_adc_lsb; + + if (ibat_adc_msb & BQ25980_ADC_POLARITY_BIT) + return ((ibat_adc ^ 0xffff) + 1) * BQ25980_ADC_CURR_STEP_uA; + + return ibat_adc * BQ25980_ADC_CURR_STEP_uA; +} + +static int bq25980_get_adc_vbat(struct bq25980_device *bq) +{ + int vsys_adc_lsb, vsys_adc_msb; + u16 vsys_adc; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_VBAT_ADC_MSB, &vsys_adc_msb); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_VBAT_ADC_LSB, &vsys_adc_lsb); + if (ret) + return ret; + + vsys_adc = (vsys_adc_msb << 8) | vsys_adc_lsb; + + return vsys_adc * BQ25980_ADC_VOLT_STEP_uV; +} + +static int bq25980_get_state(struct bq25980_device *bq, + struct bq25980_state *state) +{ + unsigned int chg_ctrl_2; + unsigned int stat1; + unsigned int stat2; + unsigned int stat3; + unsigned int stat4; + unsigned int ibat_adc_msb; + int ret; + + ret = regmap_read(bq->regmap, BQ25980_STAT1, &stat1); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_STAT2, &stat2); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_STAT3, &stat3); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_STAT4, &stat4); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_CHRGR_CTRL_2, &chg_ctrl_2); + if (ret) + return ret; + + ret = regmap_read(bq->regmap, BQ25980_IBAT_ADC_MSB, &ibat_adc_msb); + if (ret) + return ret; + + state->dischg = ibat_adc_msb & BQ25980_ADC_POLARITY_BIT; + state->ovp = (stat1 & BQ25980_STAT1_OVP_MASK) | + (stat3 & BQ25980_STAT3_OVP_MASK); + state->ocp = (stat1 & BQ25980_STAT1_OCP_MASK) | + (stat2 & BQ25980_STAT2_OCP_MASK); + state->tflt = stat4 & BQ25980_STAT4_TFLT_MASK; + state->wdt = stat4 & BQ25980_WD_STAT; + state->online = stat3 & BQ25980_PRESENT_MASK; + state->ce = chg_ctrl_2 & BQ25980_CHG_EN; + state->hiz = chg_ctrl_2 & BQ25980_EN_HIZ; + state->bypass = chg_ctrl_2 & BQ25980_EN_BYPASS; + + return 0; +} + +static int bq25980_get_battery_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq25980_device *bq = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = bq->init_data.ichg_max; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + val->intval = bq->init_data.vreg_max; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = bq25980_get_ibat_adc(bq); + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = bq25980_get_adc_vbat(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int bq25980_set_charger_property(struct power_supply *psy, + enum power_supply_property prop, + const union power_supply_propval *val) +{ + struct bq25980_device *bq = power_supply_get_drvdata(psy); + int ret = -EINVAL; + + switch (prop) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq25980_set_input_curr_lim(bq, val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + ret = bq25980_set_input_volt_lim(bq, val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = bq25980_set_bypass(bq, val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = bq25980_set_chg_en(bq, val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq25980_set_const_charge_curr(bq, val->intval); + if (ret) + return ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq25980_set_const_charge_volt(bq, val->intval); + if (ret) + return ret; + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int bq25980_get_charger_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct bq25980_device *bq = power_supply_get_drvdata(psy); + struct bq25980_state state; + int ret = 0; + + mutex_lock(&bq->lock); + ret = bq25980_get_state(bq, &state); + mutex_unlock(&bq->lock); + if (ret) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ25980_MANUFACTURER; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bq->model_name; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = state.online; + break; + + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + ret = bq25980_get_input_volt_lim(bq); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = bq25980_get_input_curr_lim(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_HEALTH: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + + if (state.tflt) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (state.ovp) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (state.ocp) + val->intval = POWER_SUPPLY_HEALTH_OVERCURRENT; + else if (state.wdt) + val->intval = + POWER_SUPPLY_HEALTH_WATCHDOG_TIMER_EXPIRE; + break; + + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + + if ((state.ce) && (!state.hiz)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (state.dischg) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (!state.ce) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + + if (!state.ce) + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + else if (state.bypass) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + else if (!state.bypass) + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = bq25980_get_adc_ibus(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = bq25980_get_adc_vbus(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = bq25980_get_const_charge_curr(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = bq25980_get_const_charge_volt(bq); + if (ret < 0) + return ret; + + val->intval = ret; + break; + + default: + return -EINVAL; + } + + return ret; +} + +static bool bq25980_state_changed(struct bq25980_device *bq, + struct bq25980_state *new_state) +{ + struct bq25980_state old_state; + + mutex_lock(&bq->lock); + old_state = bq->state; + mutex_unlock(&bq->lock); + + return (old_state.dischg != new_state->dischg || + old_state.ovp != new_state->ovp || + old_state.ocp != new_state->ocp || + old_state.online != new_state->online || + old_state.wdt != new_state->wdt || + old_state.tflt != new_state->tflt || + old_state.ce != new_state->ce || + old_state.hiz != new_state->hiz || + old_state.bypass != new_state->bypass); +} + +static irqreturn_t bq25980_irq_handler_thread(int irq, void *private) +{ + struct bq25980_device *bq = private; + struct bq25980_state state; + int ret; + + ret = bq25980_get_state(bq, &state); + if (ret < 0) + goto irq_out; + + if (!bq25980_state_changed(bq, &state)) + goto irq_out; + + mutex_lock(&bq->lock); + bq->state = state; + mutex_unlock(&bq->lock); + + power_supply_changed(bq->charger); + +irq_out: + return IRQ_HANDLED; +} + +static enum power_supply_property bq25980_power_supply_props[] = { + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static enum power_supply_property bq25980_battery_props[] = { + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static char *bq25980_charger_supplied_to[] = { + "main-battery", +}; + +static int bq25980_property_is_writeable(struct power_supply *psy, + enum power_supply_property prop) +{ + switch (prop) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_CHARGE_TYPE: + case POWER_SUPPLY_PROP_STATUS: + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + return true; + default: + return false; + } +} + +static const struct power_supply_desc bq25980_power_supply_desc = { + .name = "bq25980-charger", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = bq25980_power_supply_props, + .num_properties = ARRAY_SIZE(bq25980_power_supply_props), + .get_property = bq25980_get_charger_property, + .set_property = bq25980_set_charger_property, + .property_is_writeable = bq25980_property_is_writeable, +}; + +static struct power_supply_desc bq25980_battery_desc = { + .name = "bq25980-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = bq25980_get_battery_property, + .properties = bq25980_battery_props, + .num_properties = ARRAY_SIZE(bq25980_battery_props), + .property_is_writeable = bq25980_property_is_writeable, +}; + + +static bool bq25980_is_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case BQ25980_CHRGR_CTRL_2: + case BQ25980_STAT1...BQ25980_FLAG5: + case BQ25980_ADC_CONTROL1...BQ25980_TDIE_ADC_LSB: + return true; + default: + return false; + } +} + +static const struct regmap_config bq25980_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ25980_CHRGR_CTRL_6, + .reg_defaults = bq25980_reg_defs, + .num_reg_defaults = ARRAY_SIZE(bq25980_reg_defs), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = bq25980_is_volatile_reg, +}; + +static const struct regmap_config bq25975_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ25980_CHRGR_CTRL_6, + .reg_defaults = bq25975_reg_defs, + .num_reg_defaults = ARRAY_SIZE(bq25975_reg_defs), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = bq25980_is_volatile_reg, +}; + +static const struct regmap_config bq25960_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = BQ25980_CHRGR_CTRL_6, + .reg_defaults = bq25960_reg_defs, + .num_reg_defaults = ARRAY_SIZE(bq25960_reg_defs), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = bq25980_is_volatile_reg, +}; + +static const struct bq25980_chip_info bq25980_chip_info_tbl[] = { + [BQ25980] = { + .model_id = BQ25980, + .regmap_config = &bq25980_regmap_config, + + .busocp_def = BQ25980_BUSOCP_DFLT_uA, + .busocp_sc_min = BQ25960_BUSOCP_SC_MAX_uA, + .busocp_sc_max = BQ25980_BUSOCP_SC_MAX_uA, + .busocp_byp_max = BQ25980_BUSOCP_BYP_MAX_uA, + .busocp_byp_min = BQ25980_BUSOCP_MIN_uA, + + .busovp_sc_def = BQ25980_BUSOVP_DFLT_uV, + .busovp_byp_def = BQ25980_BUSOVP_BYPASS_DFLT_uV, + .busovp_sc_step = BQ25980_BUSOVP_SC_STEP_uV, + .busovp_sc_offset = BQ25980_BUSOVP_SC_OFFSET_uV, + .busovp_byp_step = BQ25980_BUSOVP_BYP_STEP_uV, + .busovp_byp_offset = BQ25980_BUSOVP_BYP_OFFSET_uV, + .busovp_sc_min = BQ25980_BUSOVP_SC_MIN_uV, + .busovp_sc_max = BQ25980_BUSOVP_SC_MAX_uV, + .busovp_byp_min = BQ25980_BUSOVP_BYP_MIN_uV, + .busovp_byp_max = BQ25980_BUSOVP_BYP_MAX_uV, + + .batovp_def = BQ25980_BATOVP_DFLT_uV, + .batovp_max = BQ25980_BATOVP_MAX_uV, + .batovp_min = BQ25980_BATOVP_MIN_uV, + .batovp_step = BQ25980_BATOVP_STEP_uV, + .batovp_offset = BQ25980_BATOVP_OFFSET_uV, + + .batocp_def = BQ25980_BATOCP_DFLT_uA, + .batocp_max = BQ25980_BATOCP_MAX_uA, + }, + + [BQ25975] = { + .model_id = BQ25975, + .regmap_config = &bq25975_regmap_config, + + .busocp_def = BQ25975_BUSOCP_DFLT_uA, + .busocp_sc_min = BQ25975_BUSOCP_SC_MAX_uA, + .busocp_sc_max = BQ25975_BUSOCP_SC_MAX_uA, + .busocp_byp_min = BQ25980_BUSOCP_MIN_uA, + .busocp_byp_max = BQ25975_BUSOCP_BYP_MAX_uA, + + .busovp_sc_def = BQ25975_BUSOVP_DFLT_uV, + .busovp_byp_def = BQ25975_BUSOVP_BYPASS_DFLT_uV, + .busovp_sc_step = BQ25975_BUSOVP_SC_STEP_uV, + .busovp_sc_offset = BQ25975_BUSOVP_SC_OFFSET_uV, + .busovp_byp_step = BQ25975_BUSOVP_BYP_STEP_uV, + .busovp_byp_offset = BQ25975_BUSOVP_BYP_OFFSET_uV, + .busovp_sc_min = BQ25975_BUSOVP_SC_MIN_uV, + .busovp_sc_max = BQ25975_BUSOVP_SC_MAX_uV, + .busovp_byp_min = BQ25975_BUSOVP_BYP_MIN_uV, + .busovp_byp_max = BQ25975_BUSOVP_BYP_MAX_uV, + + .batovp_def = BQ25975_BATOVP_DFLT_uV, + .batovp_max = BQ25975_BATOVP_MAX_uV, + .batovp_min = BQ25975_BATOVP_MIN_uV, + .batovp_step = BQ25975_BATOVP_STEP_uV, + .batovp_offset = BQ25975_BATOVP_OFFSET_uV, + + .batocp_def = BQ25980_BATOCP_DFLT_uA, + .batocp_max = BQ25980_BATOCP_MAX_uA, + }, + + [BQ25960] = { + .model_id = BQ25960, + .regmap_config = &bq25960_regmap_config, + + .busocp_def = BQ25960_BUSOCP_DFLT_uA, + .busocp_sc_min = BQ25960_BUSOCP_SC_MAX_uA, + .busocp_sc_max = BQ25960_BUSOCP_SC_MAX_uA, + .busocp_byp_min = BQ25960_BUSOCP_SC_MAX_uA, + .busocp_byp_max = BQ25960_BUSOCP_BYP_MAX_uA, + + .busovp_sc_def = BQ25975_BUSOVP_DFLT_uV, + .busovp_byp_def = BQ25975_BUSOVP_BYPASS_DFLT_uV, + .busovp_sc_step = BQ25960_BUSOVP_SC_STEP_uV, + .busovp_sc_offset = BQ25960_BUSOVP_SC_OFFSET_uV, + .busovp_byp_step = BQ25960_BUSOVP_BYP_STEP_uV, + .busovp_byp_offset = BQ25960_BUSOVP_BYP_OFFSET_uV, + .busovp_sc_min = BQ25960_BUSOVP_SC_MIN_uV, + .busovp_sc_max = BQ25960_BUSOVP_SC_MAX_uV, + .busovp_byp_min = BQ25960_BUSOVP_BYP_MIN_uV, + .busovp_byp_max = BQ25960_BUSOVP_BYP_MAX_uV, + + .batovp_def = BQ25960_BATOVP_DFLT_uV, + .batovp_max = BQ25960_BATOVP_MAX_uV, + .batovp_min = BQ25960_BATOVP_MIN_uV, + .batovp_step = BQ25960_BATOVP_STEP_uV, + .batovp_offset = BQ25960_BATOVP_OFFSET_uV, + + .batocp_def = BQ25960_BATOCP_DFLT_uA, + .batocp_max = BQ25960_BATOCP_MAX_uA, + }, +}; + +static int bq25980_power_supply_init(struct bq25980_device *bq, + struct device *dev) +{ + struct power_supply_config psy_cfg = { .drv_data = bq, + .of_node = dev->of_node, }; + + psy_cfg.supplied_to = bq25980_charger_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(bq25980_charger_supplied_to); + + bq->charger = devm_power_supply_register(bq->dev, + &bq25980_power_supply_desc, + &psy_cfg); + if (IS_ERR(bq->charger)) + return -EINVAL; + + bq->battery = devm_power_supply_register(bq->dev, + &bq25980_battery_desc, + &psy_cfg); + if (IS_ERR(bq->battery)) + return -EINVAL; + + return 0; +} + +static int bq25980_hw_init(struct bq25980_device *bq) +{ + struct power_supply_battery_info bat_info = { }; + int wd_reg_val = BQ25980_WATCHDOG_DIS; + int wd_max_val = BQ25980_NUM_WD_VAL - 1; + int ret = 0; + int curr_val; + int volt_val; + int i; + + if (bq->watchdog_timer) { + if (bq->watchdog_timer >= bq25980_watchdog_time[wd_max_val]) + wd_reg_val = wd_max_val; + else { + for (i = 0; i < wd_max_val; i++) { + if (bq->watchdog_timer > bq25980_watchdog_time[i] && + bq->watchdog_timer < bq25980_watchdog_time[i + 1]) { + wd_reg_val = i; + break; + } + } + } + } + + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_3, + BQ25980_WATCHDOG_MASK, wd_reg_val); + if (ret) + return ret; + + ret = power_supply_get_battery_info(bq->charger, &bat_info); + if (ret) { + dev_warn(bq->dev, "battery info missing\n"); + return -EINVAL; + } + + bq->init_data.ichg_max = bat_info.constant_charge_current_max_ua; + bq->init_data.vreg_max = bat_info.constant_charge_voltage_max_uv; + + if (bq->state.bypass) { + ret = regmap_update_bits(bq->regmap, BQ25980_CHRGR_CTRL_2, + BQ25980_EN_BYPASS, BQ25980_EN_BYPASS); + if (ret) + return ret; + + curr_val = bq->init_data.bypass_ilim; + volt_val = bq->init_data.bypass_vlim; + } else { + curr_val = bq->init_data.sc_ilim; + volt_val = bq->init_data.sc_vlim; + } + + ret = bq25980_set_input_curr_lim(bq, curr_val); + if (ret) + return ret; + + ret = bq25980_set_input_volt_lim(bq, volt_val); + if (ret) + return ret; + + return regmap_update_bits(bq->regmap, BQ25980_ADC_CONTROL1, + BQ25980_ADC_EN, BQ25980_ADC_EN); +} + +static int bq25980_parse_dt(struct bq25980_device *bq) +{ + int ret; + + ret = device_property_read_u32(bq->dev, "ti,watchdog-timeout-ms", + &bq->watchdog_timer); + if (ret) + bq->watchdog_timer = BQ25980_WATCHDOG_MIN; + + if (bq->watchdog_timer > BQ25980_WATCHDOG_MAX || + bq->watchdog_timer < BQ25980_WATCHDOG_MIN) + return -EINVAL; + + ret = device_property_read_u32(bq->dev, + "ti,sc-ovp-limit-microvolt", + &bq->init_data.sc_vlim); + if (ret) + bq->init_data.sc_vlim = bq->chip_info->busovp_sc_def; + + if (bq->init_data.sc_vlim > bq->chip_info->busovp_sc_max || + bq->init_data.sc_vlim < bq->chip_info->busovp_sc_min) { + dev_err(bq->dev, "SC ovp limit is out of range\n"); + return -EINVAL; + } + + ret = device_property_read_u32(bq->dev, + "ti,sc-ocp-limit-microamp", + &bq->init_data.sc_ilim); + if (ret) + bq->init_data.sc_ilim = bq->chip_info->busocp_def; + + if (bq->init_data.sc_ilim > bq->chip_info->busocp_sc_max || + bq->init_data.sc_ilim < bq->chip_info->busocp_sc_min) { + dev_err(bq->dev, "SC ocp limit is out of range\n"); + return -EINVAL; + } + + ret = device_property_read_u32(bq->dev, + "ti,bypass-ovp-limit-microvolt", + &bq->init_data.bypass_vlim); + if (ret) + bq->init_data.bypass_vlim = bq->chip_info->busovp_byp_def; + + if (bq->init_data.bypass_vlim > bq->chip_info->busovp_byp_max || + bq->init_data.bypass_vlim < bq->chip_info->busovp_byp_min) { + dev_err(bq->dev, "Bypass ovp limit is out of range\n"); + return -EINVAL; + } + + ret = device_property_read_u32(bq->dev, + "ti,bypass-ocp-limit-microamp", + &bq->init_data.bypass_ilim); + if (ret) + bq->init_data.bypass_ilim = bq->chip_info->busocp_def; + + if (bq->init_data.bypass_ilim > bq->chip_info->busocp_byp_max || + bq->init_data.bypass_ilim < bq->chip_info->busocp_byp_min) { + dev_err(bq->dev, "Bypass ocp limit is out of range\n"); + return -EINVAL; + } + + + bq->state.bypass = device_property_read_bool(bq->dev, + "ti,bypass-enable"); + return 0; +} + +static int bq25980_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct bq25980_device *bq; + int ret; + + bq = devm_kzalloc(dev, sizeof(*bq), GFP_KERNEL); + if (!bq) + return -ENOMEM; + + bq->client = client; + bq->dev = dev; + + mutex_init(&bq->lock); + + strncpy(bq->model_name, id->name, I2C_NAME_SIZE); + bq->chip_info = &bq25980_chip_info_tbl[id->driver_data]; + + bq->regmap = devm_regmap_init_i2c(client, + bq->chip_info->regmap_config); + if (IS_ERR(bq->regmap)) { + dev_err(dev, "Failed to allocate register map\n"); + return PTR_ERR(bq->regmap); + } + + i2c_set_clientdata(client, bq); + + ret = bq25980_parse_dt(bq); + if (ret) { + dev_err(dev, "Failed to read device tree properties%d\n", ret); + return ret; + } + + if (client->irq) { + ret = devm_request_threaded_irq(dev, client->irq, NULL, + bq25980_irq_handler_thread, + IRQF_TRIGGER_FALLING | + IRQF_ONESHOT, + dev_name(&client->dev), bq); + if (ret) + return ret; + } + + ret = bq25980_power_supply_init(bq, dev); + if (ret) { + dev_err(dev, "Failed to register power supply\n"); + return ret; + } + + ret = bq25980_hw_init(bq); + if (ret) { + dev_err(dev, "Cannot initialize the chip.\n"); + return ret; + } + + return 0; +} + +static const struct i2c_device_id bq25980_i2c_ids[] = { + { "bq25980", BQ25980 }, + { "bq25975", BQ25975 }, + { "bq25975", BQ25975 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq25980_i2c_ids); + +static const struct of_device_id bq25980_of_match[] = { + { .compatible = "ti,bq25980", .data = (void *)BQ25980 }, + { .compatible = "ti,bq25975", .data = (void *)BQ25975 }, + { .compatible = "ti,bq25960", .data = (void *)BQ25960 }, + { }, +}; +MODULE_DEVICE_TABLE(of, bq25980_of_match); + +static struct i2c_driver bq25980_driver = { + .driver = { + .name = "bq25980-charger", + .of_match_table = bq25980_of_match, + }, + .probe = bq25980_probe, + .id_table = bq25980_i2c_ids, +}; +module_i2c_driver(bq25980_driver); + +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_AUTHOR("Ricardo Rivera-Matos <r-rivera-matos@ti.com>"); +MODULE_DESCRIPTION("bq25980 charger driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/bq25980_charger.h b/drivers/power/supply/bq25980_charger.h new file mode 100644 index 000000000..39f94eba5 --- /dev/null +++ b/drivers/power/supply/bq25980_charger.h @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com/ */ + +#ifndef BQ25980_CHARGER_H +#define BQ25980_CHARGER_H + +#define BQ25980_MANUFACTURER "Texas Instruments" + +#define BQ25980_BATOVP 0x0 +#define BQ25980_BATOVP_ALM 0x1 +#define BQ25980_BATOCP 0x2 +#define BQ25980_BATOCP_ALM 0x3 +#define BQ25980_BATUCP_ALM 0x4 +#define BQ25980_CHRGR_CTRL_1 0x5 +#define BQ25980_BUSOVP 0x6 +#define BQ25980_BUSOVP_ALM 0x7 +#define BQ25980_BUSOCP 0x8 +#define BQ25980_BUSOCP_ALM 0x9 +#define BQ25980_TEMP_CONTROL 0xA +#define BQ25980_TDIE_ALM 0xB +#define BQ25980_TSBUS_FLT 0xC +#define BQ25980_TSBAT_FLG 0xD +#define BQ25980_VAC_CONTROL 0xE +#define BQ25980_CHRGR_CTRL_2 0xF +#define BQ25980_CHRGR_CTRL_3 0x10 +#define BQ25980_CHRGR_CTRL_4 0x11 +#define BQ25980_CHRGR_CTRL_5 0x12 +#define BQ25980_STAT1 0x13 +#define BQ25980_STAT2 0x14 +#define BQ25980_STAT3 0x15 +#define BQ25980_STAT4 0x16 +#define BQ25980_STAT5 0x17 +#define BQ25980_FLAG1 0x18 +#define BQ25980_FLAG2 0x19 +#define BQ25980_FLAG3 0x1A +#define BQ25980_FLAG4 0x1B +#define BQ25980_FLAG5 0x1C +#define BQ25980_MASK1 0x1D +#define BQ25980_MASK2 0x1E +#define BQ25980_MASK3 0x1F +#define BQ25980_MASK4 0x20 +#define BQ25980_MASK5 0x21 +#define BQ25980_DEVICE_INFO 0x22 +#define BQ25980_ADC_CONTROL1 0x23 +#define BQ25980_ADC_CONTROL2 0x24 +#define BQ25980_IBUS_ADC_MSB 0x25 +#define BQ25980_IBUS_ADC_LSB 0x26 +#define BQ25980_VBUS_ADC_MSB 0x27 +#define BQ25980_VBUS_ADC_LSB 0x28 +#define BQ25980_VAC1_ADC_MSB 0x29 +#define BQ25980_VAC1_ADC_LSB 0x2A +#define BQ25980_VAC2_ADC_MSB 0x2B +#define BQ25980_VAC2_ADC_LSB 0x2C +#define BQ25980_VOUT_ADC_MSB 0x2D +#define BQ25980_VOUT_ADC_LSB 0x2E +#define BQ25980_VBAT_ADC_MSB 0x2F +#define BQ25980_VBAT_ADC_LSB 0x30 +#define BQ25980_IBAT_ADC_MSB 0x31 +#define BQ25980_IBAT_ADC_LSB 0x32 +#define BQ25980_TSBUS_ADC_MSB 0x33 +#define BQ25980_TSBUS_ADC_LSB 0x34 +#define BQ25980_TSBAT_ADC_MSB 0x35 +#define BQ25980_TSBAT_ADC_LSB 0x36 +#define BQ25980_TDIE_ADC_MSB 0x37 +#define BQ25980_TDIE_ADC_LSB 0x38 +#define BQ25980_DEGLITCH_TIME 0x39 +#define BQ25980_CHRGR_CTRL_6 0x3A + +#define BQ25980_BUSOCP_STEP_uA 250000 +#define BQ25980_BUSOCP_OFFSET_uA 1000000 + +#define BQ25980_BUSOCP_DFLT_uA 4250000 +#define BQ25975_BUSOCP_DFLT_uA 4250000 +#define BQ25960_BUSOCP_DFLT_uA 3250000 + +#define BQ25980_BUSOCP_MIN_uA 1000000 + +#define BQ25980_BUSOCP_SC_MAX_uA 5750000 +#define BQ25975_BUSOCP_SC_MAX_uA 5750000 +#define BQ25960_BUSOCP_SC_MAX_uA 3750000 + +#define BQ25980_BUSOCP_BYP_MAX_uA 8500000 +#define BQ25975_BUSOCP_BYP_MAX_uA 8500000 +#define BQ25960_BUSOCP_BYP_MAX_uA 5750000 + +#define BQ25980_BUSOVP_SC_STEP_uV 100000 +#define BQ25975_BUSOVP_SC_STEP_uV 50000 +#define BQ25960_BUSOVP_SC_STEP_uV 50000 +#define BQ25980_BUSOVP_SC_OFFSET_uV 14000000 +#define BQ25975_BUSOVP_SC_OFFSET_uV 7000000 +#define BQ25960_BUSOVP_SC_OFFSET_uV 7000000 + +#define BQ25980_BUSOVP_BYP_STEP_uV 50000 +#define BQ25975_BUSOVP_BYP_STEP_uV 25000 +#define BQ25960_BUSOVP_BYP_STEP_uV 25000 +#define BQ25980_BUSOVP_BYP_OFFSET_uV 7000000 +#define BQ25975_BUSOVP_BYP_OFFSET_uV 3500000 +#define BQ25960_BUSOVP_BYP_OFFSET_uV 3500000 + +#define BQ25980_BUSOVP_DFLT_uV 17800000 +#define BQ25980_BUSOVP_BYPASS_DFLT_uV 8900000 +#define BQ25975_BUSOVP_DFLT_uV 8900000 +#define BQ25975_BUSOVP_BYPASS_DFLT_uV 4450000 +#define BQ25960_BUSOVP_DFLT_uV 8900000 + +#define BQ25980_BUSOVP_SC_MIN_uV 14000000 +#define BQ25975_BUSOVP_SC_MIN_uV 7000000 +#define BQ25960_BUSOVP_SC_MIN_uV 7000000 +#define BQ25980_BUSOVP_BYP_MIN_uV 7000000 +#define BQ25975_BUSOVP_BYP_MIN_uV 3500000 +#define BQ25960_BUSOVP_BYP_MIN_uV 3500000 + +#define BQ25980_BUSOVP_SC_MAX_uV 22000000 +#define BQ25975_BUSOVP_SC_MAX_uV 12750000 +#define BQ25960_BUSOVP_SC_MAX_uV 12750000 + +#define BQ25980_BUSOVP_BYP_MAX_uV 12750000 +#define BQ25975_BUSOVP_BYP_MAX_uV 6500000 +#define BQ25960_BUSOVP_BYP_MAX_uV 6500000 + +#define BQ25980_BATOVP_STEP_uV 20000 +#define BQ25975_BATOVP_STEP_uV 10000 +#define BQ25960_BATOVP_STEP_uV 10000 + +#define BQ25980_BATOVP_OFFSET_uV 7000000 +#define BQ25975_BATOVP_OFFSET_uV 3500000 +#define BQ25960_BATOVP_OFFSET_uV 3500000 + +#define BQ25980_BATOVP_DFLT_uV 14000000 +#define BQ25975_BATOVP_DFLT_uV 8900000 +#define BQ25960_BATOVP_DFLT_uV 8900000 + +#define BQ25980_BATOVP_MIN_uV 7000000 +#define BQ25975_BATOVP_MIN_uV 3500000 +#define BQ25960_BATOVP_MIN_uV 3500000 + +#define BQ25980_BATOVP_MAX_uV 9540000 +#define BQ25975_BATOVP_MAX_uV 4770000 +#define BQ25960_BATOVP_MAX_uV 4770000 + +#define BQ25980_BATOCP_STEP_uA 100000 + +#define BQ25980_BATOCP_MASK GENMASK(6, 0) + +#define BQ25980_BATOCP_DFLT_uA 8100000 +#define BQ25960_BATOCP_DFLT_uA 6100000 + +#define BQ25980_BATOCP_MIN_uA 2000000 + +#define BQ25980_BATOCP_MAX_uA 11000000 +#define BQ25975_BATOCP_MAX_uA 11000000 +#define BQ25960_BATOCP_MAX_uA 7000000 + +#define BQ25980_ENABLE_HIZ 0xff +#define BQ25980_DISABLE_HIZ 0x0 +#define BQ25980_EN_BYPASS BIT(3) +#define BQ25980_STAT1_OVP_MASK (BIT(6) | BIT(5) | BIT(0)) +#define BQ25980_STAT3_OVP_MASK (BIT(7) | BIT(6)) +#define BQ25980_STAT1_OCP_MASK BIT(3) +#define BQ25980_STAT2_OCP_MASK (BIT(6) | BIT(1)) +#define BQ25980_STAT4_TFLT_MASK GENMASK(5, 1) +#define BQ25980_WD_STAT BIT(0) +#define BQ25980_PRESENT_MASK GENMASK(4, 2) +#define BQ25980_CHG_EN BIT(4) +#define BQ25980_EN_HIZ BIT(6) +#define BQ25980_ADC_EN BIT(7) + +#define BQ25980_ADC_VOLT_STEP_uV 1000 +#define BQ25980_ADC_CURR_STEP_uA 1000 +#define BQ25980_ADC_POLARITY_BIT BIT(7) + +#define BQ25980_WATCHDOG_MASK GENMASK(4, 3) +#define BQ25980_WATCHDOG_DIS BIT(2) +#define BQ25980_WATCHDOG_MAX 300000 +#define BQ25980_WATCHDOG_MIN 0 +#define BQ25980_NUM_WD_VAL 4 + +#endif /* BQ25980_CHARGER_H */ diff --git a/drivers/power/supply/bq27xxx_battery.c b/drivers/power/supply/bq27xxx_battery.c new file mode 100644 index 000000000..0673e0fe0 --- /dev/null +++ b/drivers/power/supply/bq27xxx_battery.c @@ -0,0 +1,2090 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * BQ27xxx battery driver + * + * Copyright (C) 2008 Rodolfo Giometti <giometti@linux.it> + * Copyright (C) 2008 Eurotech S.p.A. <info@eurotech.it> + * Copyright (C) 2010-2011 Lars-Peter Clausen <lars@metafoo.de> + * Copyright (C) 2011 Pali Rohár <pali@kernel.org> + * Copyright (C) 2017 Liam Breck <kernel@networkimprov.net> + * + * Based on a previous work by Copyright (C) 2008 Texas Instruments, Inc. + * + * Datasheets: + * https://www.ti.com/product/bq27000 + * https://www.ti.com/product/bq27200 + * https://www.ti.com/product/bq27010 + * https://www.ti.com/product/bq27210 + * https://www.ti.com/product/bq27500 + * https://www.ti.com/product/bq27510-g1 + * https://www.ti.com/product/bq27510-g2 + * https://www.ti.com/product/bq27510-g3 + * https://www.ti.com/product/bq27520-g1 + * https://www.ti.com/product/bq27520-g2 + * https://www.ti.com/product/bq27520-g3 + * https://www.ti.com/product/bq27520-g4 + * https://www.ti.com/product/bq27530-g1 + * https://www.ti.com/product/bq27531-g1 + * https://www.ti.com/product/bq27541-g1 + * https://www.ti.com/product/bq27542-g1 + * https://www.ti.com/product/bq27546-g1 + * https://www.ti.com/product/bq27742-g1 + * https://www.ti.com/product/bq27545-g1 + * https://www.ti.com/product/bq27421-g1 + * https://www.ti.com/product/bq27425-g1 + * https://www.ti.com/product/bq27426 + * https://www.ti.com/product/bq27411-g1 + * https://www.ti.com/product/bq27441-g1 + * https://www.ti.com/product/bq27621-g1 + * https://www.ti.com/product/bq27z561 + * https://www.ti.com/product/bq28z610 + * https://www.ti.com/product/bq34z100-g1 + */ + +#include <linux/device.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/param.h> +#include <linux/jiffies.h> +#include <linux/workqueue.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/of.h> + +#include <linux/power/bq27xxx_battery.h> + +#define BQ27XXX_MANUFACTURER "Texas Instruments" + +/* BQ27XXX Flags */ +#define BQ27XXX_FLAG_DSC BIT(0) +#define BQ27XXX_FLAG_SOCF BIT(1) /* State-of-Charge threshold final */ +#define BQ27XXX_FLAG_SOC1 BIT(2) /* State-of-Charge threshold 1 */ +#define BQ27XXX_FLAG_CFGUP BIT(4) +#define BQ27XXX_FLAG_FC BIT(9) +#define BQ27XXX_FLAG_OTD BIT(14) +#define BQ27XXX_FLAG_OTC BIT(15) +#define BQ27XXX_FLAG_UT BIT(14) +#define BQ27XXX_FLAG_OT BIT(15) + +/* BQ27000 has different layout for Flags register */ +#define BQ27000_FLAG_EDVF BIT(0) /* Final End-of-Discharge-Voltage flag */ +#define BQ27000_FLAG_EDV1 BIT(1) /* First End-of-Discharge-Voltage flag */ +#define BQ27000_FLAG_CI BIT(4) /* Capacity Inaccurate flag */ +#define BQ27000_FLAG_FC BIT(5) +#define BQ27000_FLAG_CHGS BIT(7) /* Charge state flag */ + +/* BQ27Z561 has different layout for Flags register */ +#define BQ27Z561_FLAG_FDC BIT(4) /* Battery fully discharged */ +#define BQ27Z561_FLAG_FC BIT(5) /* Battery fully charged */ +#define BQ27Z561_FLAG_DIS_CH BIT(6) /* Battery is discharging */ + +/* control register params */ +#define BQ27XXX_SEALED 0x20 +#define BQ27XXX_SET_CFGUPDATE 0x13 +#define BQ27XXX_SOFT_RESET 0x42 +#define BQ27XXX_RESET 0x41 + +#define BQ27XXX_RS (20) /* Resistor sense mOhm */ +#define BQ27XXX_POWER_CONSTANT (29200) /* 29.2 µV^2 * 1000 */ +#define BQ27XXX_CURRENT_CONSTANT (3570) /* 3.57 µV * 1000 */ + +#define INVALID_REG_ADDR 0xff + +/* + * bq27xxx_reg_index - Register names + * + * These are indexes into a device's register mapping array. + */ + +enum bq27xxx_reg_index { + BQ27XXX_REG_CTRL = 0, /* Control */ + BQ27XXX_REG_TEMP, /* Temperature */ + BQ27XXX_REG_INT_TEMP, /* Internal Temperature */ + BQ27XXX_REG_VOLT, /* Voltage */ + BQ27XXX_REG_AI, /* Average Current */ + BQ27XXX_REG_FLAGS, /* Flags */ + BQ27XXX_REG_TTE, /* Time-to-Empty */ + BQ27XXX_REG_TTF, /* Time-to-Full */ + BQ27XXX_REG_TTES, /* Time-to-Empty Standby */ + BQ27XXX_REG_TTECP, /* Time-to-Empty at Constant Power */ + BQ27XXX_REG_NAC, /* Nominal Available Capacity */ + BQ27XXX_REG_FCC, /* Full Charge Capacity */ + BQ27XXX_REG_CYCT, /* Cycle Count */ + BQ27XXX_REG_AE, /* Available Energy */ + BQ27XXX_REG_SOC, /* State-of-Charge */ + BQ27XXX_REG_DCAP, /* Design Capacity */ + BQ27XXX_REG_AP, /* Average Power */ + BQ27XXX_DM_CTRL, /* Block Data Control */ + BQ27XXX_DM_CLASS, /* Data Class */ + BQ27XXX_DM_BLOCK, /* Data Block */ + BQ27XXX_DM_DATA, /* Block Data */ + BQ27XXX_DM_CKSUM, /* Block Data Checksum */ + BQ27XXX_REG_MAX, /* sentinel */ +}; + +#define BQ27XXX_DM_REG_ROWS \ + [BQ27XXX_DM_CTRL] = 0x61, \ + [BQ27XXX_DM_CLASS] = 0x3e, \ + [BQ27XXX_DM_BLOCK] = 0x3f, \ + [BQ27XXX_DM_DATA] = 0x40, \ + [BQ27XXX_DM_CKSUM] = 0x60 + +/* Register mappings */ +static u8 + bq27000_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x0b, + [BQ27XXX_REG_DCAP] = 0x76, + [BQ27XXX_REG_AP] = 0x24, + [BQ27XXX_DM_CTRL] = INVALID_REG_ADDR, + [BQ27XXX_DM_CLASS] = INVALID_REG_ADDR, + [BQ27XXX_DM_BLOCK] = INVALID_REG_ADDR, + [BQ27XXX_DM_DATA] = INVALID_REG_ADDR, + [BQ27XXX_DM_CKSUM] = INVALID_REG_ADDR, + }, + bq27010_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x0b, + [BQ27XXX_REG_DCAP] = 0x76, + [BQ27XXX_REG_AP] = INVALID_REG_ADDR, + [BQ27XXX_DM_CTRL] = INVALID_REG_ADDR, + [BQ27XXX_DM_CLASS] = INVALID_REG_ADDR, + [BQ27XXX_DM_BLOCK] = INVALID_REG_ADDR, + [BQ27XXX_DM_DATA] = INVALID_REG_ADDR, + [BQ27XXX_DM_CKSUM] = INVALID_REG_ADDR, + }, + bq2750x_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x28, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = 0x1a, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = INVALID_REG_ADDR, + BQ27XXX_DM_REG_ROWS, + }, +#define bq2751x_regs bq27510g3_regs +#define bq2752x_regs bq27510g3_regs + bq27500_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, +#define bq27510g1_regs bq27500_regs +#define bq27510g2_regs bq27500_regs + bq27510g3_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x28, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = 0x1a, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x1e, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x20, + [BQ27XXX_REG_DCAP] = 0x2e, + [BQ27XXX_REG_AP] = INVALID_REG_ADDR, + BQ27XXX_DM_REG_ROWS, + }, + bq27520g1_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = INVALID_REG_ADDR, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, + bq27520g2_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x36, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, + bq27520g3_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x36, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = 0x26, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, + bq27520g4_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x28, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = 0x1c, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x1e, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x20, + [BQ27XXX_REG_DCAP] = INVALID_REG_ADDR, + [BQ27XXX_REG_AP] = INVALID_REG_ADDR, + BQ27XXX_DM_REG_ROWS, + }, + bq27521_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x02, + [BQ27XXX_REG_TEMP] = 0x0a, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x0c, + [BQ27XXX_REG_AI] = 0x0e, + [BQ27XXX_REG_FLAGS] = 0x08, + [BQ27XXX_REG_TTE] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = INVALID_REG_ADDR, + [BQ27XXX_REG_FCC] = INVALID_REG_ADDR, + [BQ27XXX_REG_CYCT] = INVALID_REG_ADDR, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = INVALID_REG_ADDR, + [BQ27XXX_REG_DCAP] = INVALID_REG_ADDR, + [BQ27XXX_REG_AP] = INVALID_REG_ADDR, + [BQ27XXX_DM_CTRL] = INVALID_REG_ADDR, + [BQ27XXX_DM_CLASS] = INVALID_REG_ADDR, + [BQ27XXX_DM_BLOCK] = INVALID_REG_ADDR, + [BQ27XXX_DM_DATA] = INVALID_REG_ADDR, + [BQ27XXX_DM_CKSUM] = INVALID_REG_ADDR, + }, + bq27530_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x32, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = INVALID_REG_ADDR, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, +#define bq27531_regs bq27530_regs + bq27541_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x28, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, +#define bq27542_regs bq27541_regs +#define bq27546_regs bq27541_regs +#define bq27742_regs bq27541_regs + bq27545_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = 0x28, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x0c, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = INVALID_REG_ADDR, + [BQ27XXX_REG_AP] = 0x24, + BQ27XXX_DM_REG_ROWS, + }, + bq27421_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x02, + [BQ27XXX_REG_INT_TEMP] = 0x1e, + [BQ27XXX_REG_VOLT] = 0x04, + [BQ27XXX_REG_AI] = 0x10, + [BQ27XXX_REG_FLAGS] = 0x06, + [BQ27XXX_REG_TTE] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTF] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = 0x08, + [BQ27XXX_REG_FCC] = 0x0e, + [BQ27XXX_REG_CYCT] = INVALID_REG_ADDR, + [BQ27XXX_REG_AE] = INVALID_REG_ADDR, + [BQ27XXX_REG_SOC] = 0x1c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x18, + BQ27XXX_DM_REG_ROWS, + }, +#define bq27411_regs bq27421_regs +#define bq27425_regs bq27421_regs +#define bq27426_regs bq27421_regs +#define bq27441_regs bq27421_regs +#define bq27621_regs bq27421_regs + bq27z561_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = INVALID_REG_ADDR, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x22, + BQ27XXX_DM_REG_ROWS, + }, + bq28z610_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x06, + [BQ27XXX_REG_INT_TEMP] = INVALID_REG_ADDR, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x14, + [BQ27XXX_REG_FLAGS] = 0x0a, + [BQ27XXX_REG_TTE] = 0x16, + [BQ27XXX_REG_TTF] = 0x18, + [BQ27XXX_REG_TTES] = INVALID_REG_ADDR, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = INVALID_REG_ADDR, + [BQ27XXX_REG_FCC] = 0x12, + [BQ27XXX_REG_CYCT] = 0x2a, + [BQ27XXX_REG_AE] = 0x22, + [BQ27XXX_REG_SOC] = 0x2c, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x22, + BQ27XXX_DM_REG_ROWS, + }, + bq34z100_regs[BQ27XXX_REG_MAX] = { + [BQ27XXX_REG_CTRL] = 0x00, + [BQ27XXX_REG_TEMP] = 0x0c, + [BQ27XXX_REG_INT_TEMP] = 0x2a, + [BQ27XXX_REG_VOLT] = 0x08, + [BQ27XXX_REG_AI] = 0x0a, + [BQ27XXX_REG_FLAGS] = 0x0e, + [BQ27XXX_REG_TTE] = 0x18, + [BQ27XXX_REG_TTF] = 0x1a, + [BQ27XXX_REG_TTES] = 0x1e, + [BQ27XXX_REG_TTECP] = INVALID_REG_ADDR, + [BQ27XXX_REG_NAC] = INVALID_REG_ADDR, + [BQ27XXX_REG_FCC] = 0x06, + [BQ27XXX_REG_CYCT] = 0x2c, + [BQ27XXX_REG_AE] = 0x24, + [BQ27XXX_REG_SOC] = 0x02, + [BQ27XXX_REG_DCAP] = 0x3c, + [BQ27XXX_REG_AP] = 0x22, + BQ27XXX_DM_REG_ROWS, + }; + +static enum power_supply_property bq27000_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq27010_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +#define bq2750x_props bq27510g3_props +#define bq2751x_props bq27510g3_props +#define bq2752x_props bq27510g3_props + +static enum power_supply_property bq27500_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; +#define bq27510g1_props bq27500_props +#define bq27510g2_props bq27500_props + +static enum power_supply_property bq27510g3_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq27520g1_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +#define bq27520g2_props bq27500_props + +static enum power_supply_property bq27520g3_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq27520g4_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq27521_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, +}; + +static enum power_supply_property bq27530_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_MANUFACTURER, +}; +#define bq27531_props bq27530_props + +static enum power_supply_property bq27541_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; +#define bq27542_props bq27541_props +#define bq27546_props bq27541_props +#define bq27742_props bq27541_props + +static enum power_supply_property bq27545_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq27421_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_MANUFACTURER, +}; +#define bq27411_props bq27421_props +#define bq27425_props bq27421_props +#define bq27426_props bq27421_props +#define bq27441_props bq27421_props +#define bq27621_props bq27421_props + +static enum power_supply_property bq27z561_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq28z610_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static enum power_supply_property bq34z100_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +struct bq27xxx_dm_reg { + u8 subclass_id; + u8 offset; + u8 bytes; + u16 min, max; +}; + +enum bq27xxx_dm_reg_id { + BQ27XXX_DM_DESIGN_CAPACITY = 0, + BQ27XXX_DM_DESIGN_ENERGY, + BQ27XXX_DM_TERMINATE_VOLTAGE, +}; + +#define bq27000_dm_regs 0 +#define bq27010_dm_regs 0 +#define bq2750x_dm_regs 0 +#define bq2751x_dm_regs 0 +#define bq2752x_dm_regs 0 + +#if 0 /* not yet tested */ +static struct bq27xxx_dm_reg bq27500_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 48, 10, 2, 0, 65535 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { }, /* missing on chip */ + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 80, 48, 2, 1000, 32767 }, +}; +#else +#define bq27500_dm_regs 0 +#endif + +/* todo create data memory definitions from datasheets and test on chips */ +#define bq27510g1_dm_regs 0 +#define bq27510g2_dm_regs 0 +#define bq27510g3_dm_regs 0 +#define bq27520g1_dm_regs 0 +#define bq27520g2_dm_regs 0 +#define bq27520g3_dm_regs 0 +#define bq27520g4_dm_regs 0 +#define bq27521_dm_regs 0 +#define bq27530_dm_regs 0 +#define bq27531_dm_regs 0 +#define bq27541_dm_regs 0 +#define bq27542_dm_regs 0 +#define bq27546_dm_regs 0 +#define bq27742_dm_regs 0 + +#if 0 /* not yet tested */ +static struct bq27xxx_dm_reg bq27545_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 48, 23, 2, 0, 32767 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 48, 25, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 80, 67, 2, 2800, 3700 }, +}; +#else +#define bq27545_dm_regs 0 +#endif + +static struct bq27xxx_dm_reg bq27411_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 82, 10, 2, 0, 32767 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 82, 12, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 82, 16, 2, 2800, 3700 }, +}; + +static struct bq27xxx_dm_reg bq27421_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 82, 10, 2, 0, 8000 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 82, 12, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 82, 16, 2, 2500, 3700 }, +}; + +static struct bq27xxx_dm_reg bq27425_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 82, 12, 2, 0, 32767 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 82, 14, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 82, 18, 2, 2800, 3700 }, +}; + +static struct bq27xxx_dm_reg bq27426_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 82, 6, 2, 0, 8000 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 82, 8, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 82, 10, 2, 2500, 3700 }, +}; + +#if 0 /* not yet tested */ +#define bq27441_dm_regs bq27421_dm_regs +#else +#define bq27441_dm_regs 0 +#endif + +#if 0 /* not yet tested */ +static struct bq27xxx_dm_reg bq27621_dm_regs[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = { 82, 3, 2, 0, 8000 }, + [BQ27XXX_DM_DESIGN_ENERGY] = { 82, 5, 2, 0, 32767 }, + [BQ27XXX_DM_TERMINATE_VOLTAGE] = { 82, 9, 2, 2500, 3700 }, +}; +#else +#define bq27621_dm_regs 0 +#endif + +#define bq27z561_dm_regs 0 +#define bq28z610_dm_regs 0 +#define bq34z100_dm_regs 0 + +#define BQ27XXX_O_ZERO BIT(0) +#define BQ27XXX_O_OTDC BIT(1) /* has OTC/OTD overtemperature flags */ +#define BQ27XXX_O_UTOT BIT(2) /* has OT overtemperature flag */ +#define BQ27XXX_O_CFGUP BIT(3) +#define BQ27XXX_O_RAM BIT(4) +#define BQ27Z561_O_BITS BIT(5) +#define BQ27XXX_O_SOC_SI BIT(6) /* SoC is single register */ +#define BQ27XXX_O_HAS_CI BIT(7) /* has Capacity Inaccurate flag */ +#define BQ27XXX_O_MUL_CHEM BIT(8) /* multiple chemistries supported */ + +#define BQ27XXX_DATA(ref, key, opt) { \ + .opts = (opt), \ + .unseal_key = key, \ + .regs = ref##_regs, \ + .dm_regs = ref##_dm_regs, \ + .props = ref##_props, \ + .props_size = ARRAY_SIZE(ref##_props) } + +static struct { + u32 opts; + u32 unseal_key; + u8 *regs; + struct bq27xxx_dm_reg *dm_regs; + enum power_supply_property *props; + size_t props_size; +} bq27xxx_chip_data[] = { + [BQ27000] = BQ27XXX_DATA(bq27000, 0 , BQ27XXX_O_ZERO | BQ27XXX_O_SOC_SI | BQ27XXX_O_HAS_CI), + [BQ27010] = BQ27XXX_DATA(bq27010, 0 , BQ27XXX_O_ZERO | BQ27XXX_O_SOC_SI | BQ27XXX_O_HAS_CI), + [BQ2750X] = BQ27XXX_DATA(bq2750x, 0 , BQ27XXX_O_OTDC), + [BQ2751X] = BQ27XXX_DATA(bq2751x, 0 , BQ27XXX_O_OTDC), + [BQ2752X] = BQ27XXX_DATA(bq2752x, 0 , BQ27XXX_O_OTDC), + [BQ27500] = BQ27XXX_DATA(bq27500, 0x04143672, BQ27XXX_O_OTDC), + [BQ27510G1] = BQ27XXX_DATA(bq27510g1, 0 , BQ27XXX_O_OTDC), + [BQ27510G2] = BQ27XXX_DATA(bq27510g2, 0 , BQ27XXX_O_OTDC), + [BQ27510G3] = BQ27XXX_DATA(bq27510g3, 0 , BQ27XXX_O_OTDC), + [BQ27520G1] = BQ27XXX_DATA(bq27520g1, 0 , BQ27XXX_O_OTDC), + [BQ27520G2] = BQ27XXX_DATA(bq27520g2, 0 , BQ27XXX_O_OTDC), + [BQ27520G3] = BQ27XXX_DATA(bq27520g3, 0 , BQ27XXX_O_OTDC), + [BQ27520G4] = BQ27XXX_DATA(bq27520g4, 0 , BQ27XXX_O_OTDC), + [BQ27521] = BQ27XXX_DATA(bq27521, 0 , 0), + [BQ27530] = BQ27XXX_DATA(bq27530, 0 , BQ27XXX_O_UTOT), + [BQ27531] = BQ27XXX_DATA(bq27531, 0 , BQ27XXX_O_UTOT), + [BQ27541] = BQ27XXX_DATA(bq27541, 0 , BQ27XXX_O_OTDC), + [BQ27542] = BQ27XXX_DATA(bq27542, 0 , BQ27XXX_O_OTDC), + [BQ27546] = BQ27XXX_DATA(bq27546, 0 , BQ27XXX_O_OTDC), + [BQ27742] = BQ27XXX_DATA(bq27742, 0 , BQ27XXX_O_OTDC), + [BQ27545] = BQ27XXX_DATA(bq27545, 0x04143672, BQ27XXX_O_OTDC), + [BQ27411] = BQ27XXX_DATA(bq27411, 0x80008000, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP | BQ27XXX_O_RAM), + [BQ27421] = BQ27XXX_DATA(bq27421, 0x80008000, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP | BQ27XXX_O_RAM), + [BQ27425] = BQ27XXX_DATA(bq27425, 0x04143672, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP), + [BQ27426] = BQ27XXX_DATA(bq27426, 0x80008000, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP | BQ27XXX_O_RAM), + [BQ27441] = BQ27XXX_DATA(bq27441, 0x80008000, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP | BQ27XXX_O_RAM), + [BQ27621] = BQ27XXX_DATA(bq27621, 0x80008000, BQ27XXX_O_UTOT | BQ27XXX_O_CFGUP | BQ27XXX_O_RAM), + [BQ27Z561] = BQ27XXX_DATA(bq27z561, 0 , BQ27Z561_O_BITS), + [BQ28Z610] = BQ27XXX_DATA(bq28z610, 0 , BQ27Z561_O_BITS), + [BQ34Z100] = BQ27XXX_DATA(bq34z100, 0 , BQ27XXX_O_OTDC | BQ27XXX_O_SOC_SI | \ + BQ27XXX_O_HAS_CI | BQ27XXX_O_MUL_CHEM), +}; + +static DEFINE_MUTEX(bq27xxx_list_lock); +static LIST_HEAD(bq27xxx_battery_devices); + +#define BQ27XXX_MSLEEP(i) usleep_range((i)*1000, (i)*1000+500) + +#define BQ27XXX_DM_SZ 32 + +/** + * struct bq27xxx_dm_buf - chip data memory buffer + * @class: data memory subclass_id + * @block: data memory block number + * @data: data from/for the block + * @has_data: true if data has been filled by read + * @dirty: true if data has changed since last read/write + * + * Encapsulates info required to manage chip data memory blocks. + */ +struct bq27xxx_dm_buf { + u8 class; + u8 block; + u8 data[BQ27XXX_DM_SZ]; + bool has_data, dirty; +}; + +#define BQ27XXX_DM_BUF(di, i) { \ + .class = (di)->dm_regs[i].subclass_id, \ + .block = (di)->dm_regs[i].offset / BQ27XXX_DM_SZ, \ +} + +static inline u16 *bq27xxx_dm_reg_ptr(struct bq27xxx_dm_buf *buf, + struct bq27xxx_dm_reg *reg) +{ + if (buf->class == reg->subclass_id && + buf->block == reg->offset / BQ27XXX_DM_SZ) + return (u16 *) (buf->data + reg->offset % BQ27XXX_DM_SZ); + + return NULL; +} + +static const char * const bq27xxx_dm_reg_name[] = { + [BQ27XXX_DM_DESIGN_CAPACITY] = "design-capacity", + [BQ27XXX_DM_DESIGN_ENERGY] = "design-energy", + [BQ27XXX_DM_TERMINATE_VOLTAGE] = "terminate-voltage", +}; + + +static bool bq27xxx_dt_to_nvm = true; +module_param_named(dt_monitored_battery_updates_nvm, bq27xxx_dt_to_nvm, bool, 0444); +MODULE_PARM_DESC(dt_monitored_battery_updates_nvm, + "Devicetree monitored-battery config updates data memory on NVM/flash chips.\n" + "Users must set this =0 when installing a different type of battery!\n" + "Default is =1." +#ifndef CONFIG_BATTERY_BQ27XXX_DT_UPDATES_NVM + "\nSetting this affects future kernel updates, not the current configuration." +#endif +); + +static int poll_interval_param_set(const char *val, const struct kernel_param *kp) +{ + struct bq27xxx_device_info *di; + unsigned int prev_val = *(unsigned int *) kp->arg; + int ret; + + ret = param_set_uint(val, kp); + if (ret < 0 || prev_val == *(unsigned int *) kp->arg) + return ret; + + mutex_lock(&bq27xxx_list_lock); + list_for_each_entry(di, &bq27xxx_battery_devices, list) + mod_delayed_work(system_wq, &di->work, 0); + mutex_unlock(&bq27xxx_list_lock); + + return ret; +} + +static const struct kernel_param_ops param_ops_poll_interval = { + .get = param_get_uint, + .set = poll_interval_param_set, +}; + +static unsigned int poll_interval = 360; +module_param_cb(poll_interval, ¶m_ops_poll_interval, &poll_interval, 0644); +MODULE_PARM_DESC(poll_interval, + "battery poll interval in seconds - 0 disables polling"); + +/* + * Common code for BQ27xxx devices + */ + +static inline int bq27xxx_read(struct bq27xxx_device_info *di, int reg_index, + bool single) +{ + int ret; + + if (!di || di->regs[reg_index] == INVALID_REG_ADDR) + return -EINVAL; + + ret = di->bus.read(di, di->regs[reg_index], single); + if (ret < 0) + dev_dbg(di->dev, "failed to read register 0x%02x (index %d)\n", + di->regs[reg_index], reg_index); + + return ret; +} + +static inline int bq27xxx_write(struct bq27xxx_device_info *di, int reg_index, + u16 value, bool single) +{ + int ret; + + if (!di || di->regs[reg_index] == INVALID_REG_ADDR) + return -EINVAL; + + if (!di->bus.write) + return -EPERM; + + ret = di->bus.write(di, di->regs[reg_index], value, single); + if (ret < 0) + dev_dbg(di->dev, "failed to write register 0x%02x (index %d)\n", + di->regs[reg_index], reg_index); + + return ret; +} + +static inline int bq27xxx_read_block(struct bq27xxx_device_info *di, int reg_index, + u8 *data, int len) +{ + int ret; + + if (!di || di->regs[reg_index] == INVALID_REG_ADDR) + return -EINVAL; + + if (!di->bus.read_bulk) + return -EPERM; + + ret = di->bus.read_bulk(di, di->regs[reg_index], data, len); + if (ret < 0) + dev_dbg(di->dev, "failed to read_bulk register 0x%02x (index %d)\n", + di->regs[reg_index], reg_index); + + return ret; +} + +static inline int bq27xxx_write_block(struct bq27xxx_device_info *di, int reg_index, + u8 *data, int len) +{ + int ret; + + if (!di || di->regs[reg_index] == INVALID_REG_ADDR) + return -EINVAL; + + if (!di->bus.write_bulk) + return -EPERM; + + ret = di->bus.write_bulk(di, di->regs[reg_index], data, len); + if (ret < 0) + dev_dbg(di->dev, "failed to write_bulk register 0x%02x (index %d)\n", + di->regs[reg_index], reg_index); + + return ret; +} + +static int bq27xxx_battery_seal(struct bq27xxx_device_info *di) +{ + int ret; + + ret = bq27xxx_write(di, BQ27XXX_REG_CTRL, BQ27XXX_SEALED, false); + if (ret < 0) { + dev_err(di->dev, "bus error on seal: %d\n", ret); + return ret; + } + + return 0; +} + +static int bq27xxx_battery_unseal(struct bq27xxx_device_info *di) +{ + int ret; + + if (di->unseal_key == 0) { + dev_err(di->dev, "unseal failed due to missing key\n"); + return -EINVAL; + } + + ret = bq27xxx_write(di, BQ27XXX_REG_CTRL, (u16)(di->unseal_key >> 16), false); + if (ret < 0) + goto out; + + ret = bq27xxx_write(di, BQ27XXX_REG_CTRL, (u16)di->unseal_key, false); + if (ret < 0) + goto out; + + return 0; + +out: + dev_err(di->dev, "bus error on unseal: %d\n", ret); + return ret; +} + +static u8 bq27xxx_battery_checksum_dm_block(struct bq27xxx_dm_buf *buf) +{ + u16 sum = 0; + int i; + + for (i = 0; i < BQ27XXX_DM_SZ; i++) + sum += buf->data[i]; + sum &= 0xff; + + return 0xff - sum; +} + +static int bq27xxx_battery_read_dm_block(struct bq27xxx_device_info *di, + struct bq27xxx_dm_buf *buf) +{ + int ret; + + buf->has_data = false; + + ret = bq27xxx_write(di, BQ27XXX_DM_CLASS, buf->class, true); + if (ret < 0) + goto out; + + ret = bq27xxx_write(di, BQ27XXX_DM_BLOCK, buf->block, true); + if (ret < 0) + goto out; + + BQ27XXX_MSLEEP(1); + + ret = bq27xxx_read_block(di, BQ27XXX_DM_DATA, buf->data, BQ27XXX_DM_SZ); + if (ret < 0) + goto out; + + ret = bq27xxx_read(di, BQ27XXX_DM_CKSUM, true); + if (ret < 0) + goto out; + + if ((u8)ret != bq27xxx_battery_checksum_dm_block(buf)) { + ret = -EINVAL; + goto out; + } + + buf->has_data = true; + buf->dirty = false; + + return 0; + +out: + dev_err(di->dev, "bus error reading chip memory: %d\n", ret); + return ret; +} + +static void bq27xxx_battery_update_dm_block(struct bq27xxx_device_info *di, + struct bq27xxx_dm_buf *buf, + enum bq27xxx_dm_reg_id reg_id, + unsigned int val) +{ + struct bq27xxx_dm_reg *reg = &di->dm_regs[reg_id]; + const char *str = bq27xxx_dm_reg_name[reg_id]; + u16 *prev = bq27xxx_dm_reg_ptr(buf, reg); + + if (prev == NULL) { + dev_warn(di->dev, "buffer does not match %s dm spec\n", str); + return; + } + + if (reg->bytes != 2) { + dev_warn(di->dev, "%s dm spec has unsupported byte size\n", str); + return; + } + + if (!buf->has_data) + return; + + if (be16_to_cpup(prev) == val) { + dev_info(di->dev, "%s has %u\n", str, val); + return; + } + +#ifdef CONFIG_BATTERY_BQ27XXX_DT_UPDATES_NVM + if (!(di->opts & BQ27XXX_O_RAM) && !bq27xxx_dt_to_nvm) { +#else + if (!(di->opts & BQ27XXX_O_RAM)) { +#endif + /* devicetree and NVM differ; defer to NVM */ + dev_warn(di->dev, "%s has %u; update to %u disallowed " +#ifdef CONFIG_BATTERY_BQ27XXX_DT_UPDATES_NVM + "by dt_monitored_battery_updates_nvm=0" +#else + "for flash/NVM data memory" +#endif + "\n", str, be16_to_cpup(prev), val); + return; + } + + dev_info(di->dev, "update %s to %u\n", str, val); + + *prev = cpu_to_be16(val); + buf->dirty = true; +} + +static int bq27xxx_battery_cfgupdate_priv(struct bq27xxx_device_info *di, bool active) +{ + const int limit = 100; + u16 cmd = active ? BQ27XXX_SET_CFGUPDATE : BQ27XXX_SOFT_RESET; + int ret, try = limit; + + ret = bq27xxx_write(di, BQ27XXX_REG_CTRL, cmd, false); + if (ret < 0) + return ret; + + do { + BQ27XXX_MSLEEP(25); + ret = bq27xxx_read(di, BQ27XXX_REG_FLAGS, false); + if (ret < 0) + return ret; + } while (!!(ret & BQ27XXX_FLAG_CFGUP) != active && --try); + + if (!try && di->chip != BQ27425) { // 425 has a bug + dev_err(di->dev, "timed out waiting for cfgupdate flag %d\n", active); + return -EINVAL; + } + + if (limit - try > 3) + dev_warn(di->dev, "cfgupdate %d, retries %d\n", active, limit - try); + + return 0; +} + +static inline int bq27xxx_battery_set_cfgupdate(struct bq27xxx_device_info *di) +{ + int ret = bq27xxx_battery_cfgupdate_priv(di, true); + if (ret < 0 && ret != -EINVAL) + dev_err(di->dev, "bus error on set_cfgupdate: %d\n", ret); + + return ret; +} + +static inline int bq27xxx_battery_soft_reset(struct bq27xxx_device_info *di) +{ + int ret = bq27xxx_battery_cfgupdate_priv(di, false); + if (ret < 0 && ret != -EINVAL) + dev_err(di->dev, "bus error on soft_reset: %d\n", ret); + + return ret; +} + +static int bq27xxx_battery_write_dm_block(struct bq27xxx_device_info *di, + struct bq27xxx_dm_buf *buf) +{ + bool cfgup = di->opts & BQ27XXX_O_CFGUP; + int ret; + + if (!buf->dirty) + return 0; + + if (cfgup) { + ret = bq27xxx_battery_set_cfgupdate(di); + if (ret < 0) + return ret; + } + + ret = bq27xxx_write(di, BQ27XXX_DM_CTRL, 0, true); + if (ret < 0) + goto out; + + ret = bq27xxx_write(di, BQ27XXX_DM_CLASS, buf->class, true); + if (ret < 0) + goto out; + + ret = bq27xxx_write(di, BQ27XXX_DM_BLOCK, buf->block, true); + if (ret < 0) + goto out; + + BQ27XXX_MSLEEP(1); + + ret = bq27xxx_write_block(di, BQ27XXX_DM_DATA, buf->data, BQ27XXX_DM_SZ); + if (ret < 0) + goto out; + + ret = bq27xxx_write(di, BQ27XXX_DM_CKSUM, + bq27xxx_battery_checksum_dm_block(buf), true); + if (ret < 0) + goto out; + + /* DO NOT read BQ27XXX_DM_CKSUM here to verify it! That may cause NVM + * corruption on the '425 chip (and perhaps others), which can damage + * the chip. + */ + + if (cfgup) { + BQ27XXX_MSLEEP(1); + ret = bq27xxx_battery_soft_reset(di); + if (ret < 0) + return ret; + } else { + BQ27XXX_MSLEEP(100); /* flash DM updates in <100ms */ + } + + buf->dirty = false; + + return 0; + +out: + if (cfgup) + bq27xxx_battery_soft_reset(di); + + dev_err(di->dev, "bus error writing chip memory: %d\n", ret); + return ret; +} + +static void bq27xxx_battery_set_config(struct bq27xxx_device_info *di, + struct power_supply_battery_info *info) +{ + struct bq27xxx_dm_buf bd = BQ27XXX_DM_BUF(di, BQ27XXX_DM_DESIGN_CAPACITY); + struct bq27xxx_dm_buf bt = BQ27XXX_DM_BUF(di, BQ27XXX_DM_TERMINATE_VOLTAGE); + bool updated; + + if (bq27xxx_battery_unseal(di) < 0) + return; + + if (info->charge_full_design_uah != -EINVAL && + info->energy_full_design_uwh != -EINVAL) { + bq27xxx_battery_read_dm_block(di, &bd); + /* assume design energy & capacity are in same block */ + bq27xxx_battery_update_dm_block(di, &bd, + BQ27XXX_DM_DESIGN_CAPACITY, + info->charge_full_design_uah / 1000); + bq27xxx_battery_update_dm_block(di, &bd, + BQ27XXX_DM_DESIGN_ENERGY, + info->energy_full_design_uwh / 1000); + } + + if (info->voltage_min_design_uv != -EINVAL) { + bool same = bd.class == bt.class && bd.block == bt.block; + if (!same) + bq27xxx_battery_read_dm_block(di, &bt); + bq27xxx_battery_update_dm_block(di, same ? &bd : &bt, + BQ27XXX_DM_TERMINATE_VOLTAGE, + info->voltage_min_design_uv / 1000); + } + + updated = bd.dirty || bt.dirty; + + bq27xxx_battery_write_dm_block(di, &bd); + bq27xxx_battery_write_dm_block(di, &bt); + + bq27xxx_battery_seal(di); + + if (updated && !(di->opts & BQ27XXX_O_CFGUP)) { + bq27xxx_write(di, BQ27XXX_REG_CTRL, BQ27XXX_RESET, false); + BQ27XXX_MSLEEP(300); /* reset time is not documented */ + } + /* assume bq27xxx_battery_update() is called hereafter */ +} + +static void bq27xxx_battery_settings(struct bq27xxx_device_info *di) +{ + struct power_supply_battery_info info = {}; + unsigned int min, max; + + if (power_supply_get_battery_info(di->bat, &info) < 0) + return; + + if (!di->dm_regs) { + dev_warn(di->dev, "data memory update not supported for chip\n"); + return; + } + + if (info.energy_full_design_uwh != info.charge_full_design_uah) { + if (info.energy_full_design_uwh == -EINVAL) + dev_warn(di->dev, "missing battery:energy-full-design-microwatt-hours\n"); + else if (info.charge_full_design_uah == -EINVAL) + dev_warn(di->dev, "missing battery:charge-full-design-microamp-hours\n"); + } + + /* assume min == 0 */ + max = di->dm_regs[BQ27XXX_DM_DESIGN_ENERGY].max; + if (info.energy_full_design_uwh > max * 1000) { + dev_err(di->dev, "invalid battery:energy-full-design-microwatt-hours %d\n", + info.energy_full_design_uwh); + info.energy_full_design_uwh = -EINVAL; + } + + /* assume min == 0 */ + max = di->dm_regs[BQ27XXX_DM_DESIGN_CAPACITY].max; + if (info.charge_full_design_uah > max * 1000) { + dev_err(di->dev, "invalid battery:charge-full-design-microamp-hours %d\n", + info.charge_full_design_uah); + info.charge_full_design_uah = -EINVAL; + } + + min = di->dm_regs[BQ27XXX_DM_TERMINATE_VOLTAGE].min; + max = di->dm_regs[BQ27XXX_DM_TERMINATE_VOLTAGE].max; + if ((info.voltage_min_design_uv < min * 1000 || + info.voltage_min_design_uv > max * 1000) && + info.voltage_min_design_uv != -EINVAL) { + dev_err(di->dev, "invalid battery:voltage-min-design-microvolt %d\n", + info.voltage_min_design_uv); + info.voltage_min_design_uv = -EINVAL; + } + + if ((info.energy_full_design_uwh != -EINVAL && + info.charge_full_design_uah != -EINVAL) || + info.voltage_min_design_uv != -EINVAL) + bq27xxx_battery_set_config(di, &info); +} + +/* + * Return the battery State-of-Charge + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_soc(struct bq27xxx_device_info *di) +{ + int soc; + + if (di->opts & BQ27XXX_O_SOC_SI) + soc = bq27xxx_read(di, BQ27XXX_REG_SOC, true); + else + soc = bq27xxx_read(di, BQ27XXX_REG_SOC, false); + + if (soc < 0) + dev_dbg(di->dev, "error reading State-of-Charge\n"); + + return soc; +} + +/* + * Return a battery charge value in µAh + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_charge(struct bq27xxx_device_info *di, u8 reg) +{ + int charge; + + charge = bq27xxx_read(di, reg, false); + if (charge < 0) { + dev_dbg(di->dev, "error reading charge register %02x: %d\n", + reg, charge); + return charge; + } + + if (di->opts & BQ27XXX_O_ZERO) + charge *= BQ27XXX_CURRENT_CONSTANT / BQ27XXX_RS; + else + charge *= 1000; + + return charge; +} + +/* + * Return the battery Nominal available capacity in µAh + * Or < 0 if something fails. + */ +static inline int bq27xxx_battery_read_nac(struct bq27xxx_device_info *di) +{ + return bq27xxx_battery_read_charge(di, BQ27XXX_REG_NAC); +} + +/* + * Return the battery Full Charge Capacity in µAh + * Or < 0 if something fails. + */ +static inline int bq27xxx_battery_read_fcc(struct bq27xxx_device_info *di) +{ + return bq27xxx_battery_read_charge(di, BQ27XXX_REG_FCC); +} + +/* + * Return the Design Capacity in µAh + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_dcap(struct bq27xxx_device_info *di) +{ + int dcap; + + if (di->opts & BQ27XXX_O_ZERO) + dcap = bq27xxx_read(di, BQ27XXX_REG_DCAP, true); + else + dcap = bq27xxx_read(di, BQ27XXX_REG_DCAP, false); + + if (dcap < 0) { + dev_dbg(di->dev, "error reading initial last measured discharge\n"); + return dcap; + } + + if (di->opts & BQ27XXX_O_ZERO) + dcap = (dcap << 8) * BQ27XXX_CURRENT_CONSTANT / BQ27XXX_RS; + else + dcap *= 1000; + + return dcap; +} + +/* + * Return the battery Available energy in µWh + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_energy(struct bq27xxx_device_info *di) +{ + int ae; + + ae = bq27xxx_read(di, BQ27XXX_REG_AE, false); + if (ae < 0) { + dev_dbg(di->dev, "error reading available energy\n"); + return ae; + } + + if (di->opts & BQ27XXX_O_ZERO) + ae *= BQ27XXX_POWER_CONSTANT / BQ27XXX_RS; + else + ae *= 1000; + + return ae; +} + +/* + * Return the battery temperature in tenths of degree Kelvin + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_temperature(struct bq27xxx_device_info *di) +{ + int temp; + + temp = bq27xxx_read(di, BQ27XXX_REG_TEMP, false); + if (temp < 0) { + dev_err(di->dev, "error reading temperature\n"); + return temp; + } + + if (di->opts & BQ27XXX_O_ZERO) + temp = 5 * temp / 2; + + return temp; +} + +/* + * Return the battery Cycle count total + * Or < 0 if something fails. + */ +static int bq27xxx_battery_read_cyct(struct bq27xxx_device_info *di) +{ + int cyct; + + cyct = bq27xxx_read(di, BQ27XXX_REG_CYCT, false); + if (cyct < 0) + dev_err(di->dev, "error reading cycle count total\n"); + + return cyct; +} + +/* + * Read a time register. + * Return < 0 if something fails. + */ +static int bq27xxx_battery_read_time(struct bq27xxx_device_info *di, u8 reg) +{ + int tval; + + tval = bq27xxx_read(di, reg, false); + if (tval < 0) { + dev_dbg(di->dev, "error reading time register %02x: %d\n", + reg, tval); + return tval; + } + + if (tval == 65535) + return -ENODATA; + + return tval * 60; +} + +/* + * Returns true if a battery over temperature condition is detected + */ +static bool bq27xxx_battery_overtemp(struct bq27xxx_device_info *di, u16 flags) +{ + if (di->opts & BQ27XXX_O_OTDC) + return flags & (BQ27XXX_FLAG_OTC | BQ27XXX_FLAG_OTD); + if (di->opts & BQ27XXX_O_UTOT) + return flags & BQ27XXX_FLAG_OT; + + return false; +} + +/* + * Returns true if a battery under temperature condition is detected + */ +static bool bq27xxx_battery_undertemp(struct bq27xxx_device_info *di, u16 flags) +{ + if (di->opts & BQ27XXX_O_UTOT) + return flags & BQ27XXX_FLAG_UT; + + return false; +} + +/* + * Returns true if a low state of charge condition is detected + */ +static bool bq27xxx_battery_dead(struct bq27xxx_device_info *di, u16 flags) +{ + if (di->opts & BQ27XXX_O_ZERO) + return flags & (BQ27000_FLAG_EDV1 | BQ27000_FLAG_EDVF); + else if (di->opts & BQ27Z561_O_BITS) + return flags & BQ27Z561_FLAG_FDC; + else + return flags & (BQ27XXX_FLAG_SOC1 | BQ27XXX_FLAG_SOCF); +} + +/* + * Returns true if reported battery capacity is inaccurate + */ +static bool bq27xxx_battery_capacity_inaccurate(struct bq27xxx_device_info *di, + u16 flags) +{ + if (di->opts & BQ27XXX_O_HAS_CI) + return (flags & BQ27000_FLAG_CI); + else + return false; +} + +static int bq27xxx_battery_read_health(struct bq27xxx_device_info *di) +{ + /* Unlikely but important to return first */ + if (unlikely(bq27xxx_battery_overtemp(di, di->cache.flags))) + return POWER_SUPPLY_HEALTH_OVERHEAT; + if (unlikely(bq27xxx_battery_undertemp(di, di->cache.flags))) + return POWER_SUPPLY_HEALTH_COLD; + if (unlikely(bq27xxx_battery_dead(di, di->cache.flags))) + return POWER_SUPPLY_HEALTH_DEAD; + if (unlikely(bq27xxx_battery_capacity_inaccurate(di, di->cache.flags))) + return POWER_SUPPLY_HEALTH_CALIBRATION_REQUIRED; + + return POWER_SUPPLY_HEALTH_GOOD; +} + +static bool bq27xxx_battery_is_full(struct bq27xxx_device_info *di, int flags) +{ + if (di->opts & BQ27XXX_O_ZERO) + return (flags & BQ27000_FLAG_FC); + else if (di->opts & BQ27Z561_O_BITS) + return (flags & BQ27Z561_FLAG_FC); + else + return (flags & BQ27XXX_FLAG_FC); +} + +/* + * Return the battery average current in µA and the status + * Note that current can be negative signed as well + * Or 0 if something fails. + */ +static int bq27xxx_battery_current_and_status( + struct bq27xxx_device_info *di, + union power_supply_propval *val_curr, + union power_supply_propval *val_status, + struct bq27xxx_reg_cache *cache) +{ + bool single_flags = (di->opts & BQ27XXX_O_ZERO); + int curr; + int flags; + + curr = bq27xxx_read(di, BQ27XXX_REG_AI, false); + if (curr < 0) { + dev_err(di->dev, "error reading current\n"); + return curr; + } + + if (cache) { + flags = cache->flags; + } else { + flags = bq27xxx_read(di, BQ27XXX_REG_FLAGS, single_flags); + if (flags < 0) { + dev_err(di->dev, "error reading flags\n"); + return flags; + } + } + + if (di->opts & BQ27XXX_O_ZERO) { + if (!(flags & BQ27000_FLAG_CHGS)) { + dev_dbg(di->dev, "negative current!\n"); + curr = -curr; + } + + curr = curr * BQ27XXX_CURRENT_CONSTANT / BQ27XXX_RS; + } else { + /* Other gauges return signed value */ + curr = (int)((s16)curr) * 1000; + } + + if (val_curr) + val_curr->intval = curr; + + if (val_status) { + if (curr > 0) { + val_status->intval = POWER_SUPPLY_STATUS_CHARGING; + } else if (curr < 0) { + val_status->intval = POWER_SUPPLY_STATUS_DISCHARGING; + } else { + if (bq27xxx_battery_is_full(di, flags)) + val_status->intval = POWER_SUPPLY_STATUS_FULL; + else + val_status->intval = + POWER_SUPPLY_STATUS_NOT_CHARGING; + } + } + + return 0; +} + +static void bq27xxx_battery_update_unlocked(struct bq27xxx_device_info *di) +{ + union power_supply_propval status = di->last_status; + struct bq27xxx_reg_cache cache = {0, }; + bool has_singe_flag = di->opts & BQ27XXX_O_ZERO; + + cache.flags = bq27xxx_read(di, BQ27XXX_REG_FLAGS, has_singe_flag); + if ((cache.flags & 0xff) == 0xff) + cache.flags = -1; /* read error */ + if (cache.flags >= 0) { + cache.temperature = bq27xxx_battery_read_temperature(di); + if (di->regs[BQ27XXX_REG_TTE] != INVALID_REG_ADDR) + cache.time_to_empty = bq27xxx_battery_read_time(di, BQ27XXX_REG_TTE); + if (di->regs[BQ27XXX_REG_TTECP] != INVALID_REG_ADDR) + cache.time_to_empty_avg = bq27xxx_battery_read_time(di, BQ27XXX_REG_TTECP); + if (di->regs[BQ27XXX_REG_TTF] != INVALID_REG_ADDR) + cache.time_to_full = bq27xxx_battery_read_time(di, BQ27XXX_REG_TTF); + + cache.charge_full = bq27xxx_battery_read_fcc(di); + cache.capacity = bq27xxx_battery_read_soc(di); + if (di->regs[BQ27XXX_REG_AE] != INVALID_REG_ADDR) + cache.energy = bq27xxx_battery_read_energy(di); + di->cache.flags = cache.flags; + cache.health = bq27xxx_battery_read_health(di); + if (di->regs[BQ27XXX_REG_CYCT] != INVALID_REG_ADDR) + cache.cycle_count = bq27xxx_battery_read_cyct(di); + + /* + * On gauges with signed current reporting the current must be + * checked to detect charging <-> discharging status changes. + */ + if (!(di->opts & BQ27XXX_O_ZERO)) + bq27xxx_battery_current_and_status(di, NULL, &status, &cache); + + /* We only have to read charge design full once */ + if (di->charge_design_full <= 0) + di->charge_design_full = bq27xxx_battery_read_dcap(di); + } + + if ((di->cache.capacity != cache.capacity) || + (di->cache.flags != cache.flags) || + (di->last_status.intval != status.intval)) { + di->last_status.intval = status.intval; + power_supply_changed(di->bat); + } + + if (memcmp(&di->cache, &cache, sizeof(cache)) != 0) + di->cache = cache; + + di->last_update = jiffies; + + if (!di->removed && poll_interval > 0) + mod_delayed_work(system_wq, &di->work, poll_interval * HZ); +} + +void bq27xxx_battery_update(struct bq27xxx_device_info *di) +{ + mutex_lock(&di->lock); + bq27xxx_battery_update_unlocked(di); + mutex_unlock(&di->lock); +} +EXPORT_SYMBOL_GPL(bq27xxx_battery_update); + +static void bq27xxx_battery_poll(struct work_struct *work) +{ + struct bq27xxx_device_info *di = + container_of(work, struct bq27xxx_device_info, + work.work); + + bq27xxx_battery_update(di); +} + +/* + * Get the average power in µW + * Return < 0 if something fails. + */ +static int bq27xxx_battery_pwr_avg(struct bq27xxx_device_info *di, + union power_supply_propval *val) +{ + int power; + + power = bq27xxx_read(di, BQ27XXX_REG_AP, false); + if (power < 0) { + dev_err(di->dev, + "error reading average power register %02x: %d\n", + BQ27XXX_REG_AP, power); + return power; + } + + if (di->opts & BQ27XXX_O_ZERO) + val->intval = (power * BQ27XXX_POWER_CONSTANT) / BQ27XXX_RS; + else + /* Other gauges return a signed value in units of 10mW */ + val->intval = (int)((s16)power) * 10000; + + return 0; +} + +static int bq27xxx_battery_capacity_level(struct bq27xxx_device_info *di, + union power_supply_propval *val) +{ + int level; + + if (di->opts & BQ27XXX_O_ZERO) { + if (di->cache.flags & BQ27000_FLAG_FC) + level = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (di->cache.flags & BQ27000_FLAG_EDV1) + level = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (di->cache.flags & BQ27000_FLAG_EDVF) + level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else + level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + } else if (di->opts & BQ27Z561_O_BITS) { + if (di->cache.flags & BQ27Z561_FLAG_FC) + level = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (di->cache.flags & BQ27Z561_FLAG_FDC) + level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else + level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + } else { + if (di->cache.flags & BQ27XXX_FLAG_FC) + level = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (di->cache.flags & BQ27XXX_FLAG_SOC1) + level = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (di->cache.flags & BQ27XXX_FLAG_SOCF) + level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else + level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + } + + val->intval = level; + + return 0; +} + +/* + * Return the battery Voltage in millivolts + * Or < 0 if something fails. + */ +static int bq27xxx_battery_voltage(struct bq27xxx_device_info *di, + union power_supply_propval *val) +{ + int volt; + + volt = bq27xxx_read(di, BQ27XXX_REG_VOLT, false); + if (volt < 0) { + dev_err(di->dev, "error reading voltage\n"); + return volt; + } + + val->intval = volt * 1000; + + return 0; +} + +static int bq27xxx_simple_value(int value, + union power_supply_propval *val) +{ + if (value < 0) + return value; + + val->intval = value; + + return 0; +} + +static int bq27xxx_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct bq27xxx_device_info *di = power_supply_get_drvdata(psy); + + mutex_lock(&di->lock); + if (time_is_before_jiffies(di->last_update + 5 * HZ)) + bq27xxx_battery_update_unlocked(di); + mutex_unlock(&di->lock); + + if (psp != POWER_SUPPLY_PROP_PRESENT && di->cache.flags < 0) + return -ENODEV; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = bq27xxx_battery_current_and_status(di, NULL, val, NULL); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = bq27xxx_battery_voltage(di, val); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = di->cache.flags < 0 ? 0 : 1; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = bq27xxx_battery_current_and_status(di, val, NULL, NULL); + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = bq27xxx_simple_value(di->cache.capacity, val); + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + ret = bq27xxx_battery_capacity_level(di, val); + break; + case POWER_SUPPLY_PROP_TEMP: + ret = bq27xxx_simple_value(di->cache.temperature, val); + if (ret == 0) + val->intval -= 2731; /* convert decidegree k to c */ + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + ret = bq27xxx_simple_value(di->cache.time_to_empty, val); + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + ret = bq27xxx_simple_value(di->cache.time_to_empty_avg, val); + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + ret = bq27xxx_simple_value(di->cache.time_to_full, val); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + if (di->opts & BQ27XXX_O_MUL_CHEM) + val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + else + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = bq27xxx_simple_value(bq27xxx_battery_read_nac(di), val); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = bq27xxx_simple_value(di->cache.charge_full, val); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = bq27xxx_simple_value(di->charge_design_full, val); + break; + /* + * TODO: Implement these to make registers set from + * power_supply_battery_info visible in sysfs. + */ + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + return -EINVAL; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + ret = bq27xxx_simple_value(di->cache.cycle_count, val); + break; + case POWER_SUPPLY_PROP_ENERGY_NOW: + ret = bq27xxx_simple_value(di->cache.energy, val); + break; + case POWER_SUPPLY_PROP_POWER_AVG: + ret = bq27xxx_battery_pwr_avg(di, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = bq27xxx_simple_value(di->cache.health, val); + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = BQ27XXX_MANUFACTURER; + break; + default: + return -EINVAL; + } + + return ret; +} + +static void bq27xxx_external_power_changed(struct power_supply *psy) +{ + struct bq27xxx_device_info *di = power_supply_get_drvdata(psy); + + /* After charger plug in/out wait 0.5s for things to stabilize */ + mod_delayed_work(system_wq, &di->work, HZ / 2); +} + +int bq27xxx_battery_setup(struct bq27xxx_device_info *di) +{ + struct power_supply_desc *psy_desc; + struct power_supply_config psy_cfg = { + .of_node = di->dev->of_node, + .drv_data = di, + }; + + INIT_DELAYED_WORK(&di->work, bq27xxx_battery_poll); + mutex_init(&di->lock); + + di->regs = bq27xxx_chip_data[di->chip].regs; + di->unseal_key = bq27xxx_chip_data[di->chip].unseal_key; + di->dm_regs = bq27xxx_chip_data[di->chip].dm_regs; + di->opts = bq27xxx_chip_data[di->chip].opts; + + psy_desc = devm_kzalloc(di->dev, sizeof(*psy_desc), GFP_KERNEL); + if (!psy_desc) + return -ENOMEM; + + psy_desc->name = di->name; + psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; + psy_desc->properties = bq27xxx_chip_data[di->chip].props; + psy_desc->num_properties = bq27xxx_chip_data[di->chip].props_size; + psy_desc->get_property = bq27xxx_battery_get_property; + psy_desc->external_power_changed = bq27xxx_external_power_changed; + + di->bat = power_supply_register_no_ws(di->dev, psy_desc, &psy_cfg); + if (IS_ERR(di->bat)) + return dev_err_probe(di->dev, PTR_ERR(di->bat), + "failed to register battery\n"); + + bq27xxx_battery_settings(di); + bq27xxx_battery_update(di); + + mutex_lock(&bq27xxx_list_lock); + list_add(&di->list, &bq27xxx_battery_devices); + mutex_unlock(&bq27xxx_list_lock); + + return 0; +} +EXPORT_SYMBOL_GPL(bq27xxx_battery_setup); + +void bq27xxx_battery_teardown(struct bq27xxx_device_info *di) +{ + mutex_lock(&bq27xxx_list_lock); + list_del(&di->list); + mutex_unlock(&bq27xxx_list_lock); + + /* Set removed to avoid bq27xxx_battery_update() re-queuing the work */ + mutex_lock(&di->lock); + di->removed = true; + mutex_unlock(&di->lock); + + cancel_delayed_work_sync(&di->work); + + power_supply_unregister(di->bat); + mutex_destroy(&di->lock); +} +EXPORT_SYMBOL_GPL(bq27xxx_battery_teardown); + +MODULE_AUTHOR("Rodolfo Giometti <giometti@linux.it>"); +MODULE_DESCRIPTION("BQ27xxx battery monitor driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/bq27xxx_battery_hdq.c b/drivers/power/supply/bq27xxx_battery_hdq.c new file mode 100644 index 000000000..922759ab2 --- /dev/null +++ b/drivers/power/supply/bq27xxx_battery_hdq.c @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * BQ27xxx battery monitor HDQ/1-wire driver + * + * Copyright (C) 2007-2017 Texas Instruments Incorporated - https://www.ti.com/ + * + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/types.h> +#include <linux/platform_device.h> +#include <linux/mutex.h> +#include <linux/power/bq27xxx_battery.h> + +#include <linux/w1.h> + +#define W1_FAMILY_BQ27000 0x01 + +#define HDQ_CMD_READ (0 << 7) +#define HDQ_CMD_WRITE (1 << 7) + +static int F_ID; +module_param(F_ID, int, S_IRUSR); +MODULE_PARM_DESC(F_ID, "1-wire slave FID for BQ27xxx device"); + +static int w1_bq27000_read(struct w1_slave *sl, unsigned int reg) +{ + u8 val; + + mutex_lock(&sl->master->bus_mutex); + w1_write_8(sl->master, HDQ_CMD_READ | reg); + val = w1_read_8(sl->master); + mutex_unlock(&sl->master->bus_mutex); + + return val; +} + +static int bq27xxx_battery_hdq_read(struct bq27xxx_device_info *di, u8 reg, + bool single) +{ + struct w1_slave *sl = dev_to_w1_slave(di->dev); + unsigned int timeout = 3; + int upper, lower; + int temp; + + if (!single) { + /* + * Make sure the value has not changed in between reading the + * lower and the upper part + */ + upper = w1_bq27000_read(sl, reg + 1); + do { + temp = upper; + if (upper < 0) + return upper; + + lower = w1_bq27000_read(sl, reg); + if (lower < 0) + return lower; + + upper = w1_bq27000_read(sl, reg + 1); + } while (temp != upper && --timeout); + + if (timeout == 0) + return -EIO; + + return (upper << 8) | lower; + } + + return w1_bq27000_read(sl, reg); +} + +static int bq27xxx_battery_hdq_add_slave(struct w1_slave *sl) +{ + struct bq27xxx_device_info *di; + + di = devm_kzalloc(&sl->dev, sizeof(*di), GFP_KERNEL); + if (!di) + return -ENOMEM; + + dev_set_drvdata(&sl->dev, di); + + di->dev = &sl->dev; + di->chip = BQ27000; + di->name = "bq27000-battery"; + di->bus.read = bq27xxx_battery_hdq_read; + + return bq27xxx_battery_setup(di); +} + +static void bq27xxx_battery_hdq_remove_slave(struct w1_slave *sl) +{ + struct bq27xxx_device_info *di = dev_get_drvdata(&sl->dev); + + bq27xxx_battery_teardown(di); +} + +static const struct w1_family_ops bq27xxx_battery_hdq_fops = { + .add_slave = bq27xxx_battery_hdq_add_slave, + .remove_slave = bq27xxx_battery_hdq_remove_slave, +}; + +static struct w1_family bq27xxx_battery_hdq_family = { + .fid = W1_FAMILY_BQ27000, + .fops = &bq27xxx_battery_hdq_fops, +}; + +static int __init bq27xxx_battery_hdq_init(void) +{ + if (F_ID) + bq27xxx_battery_hdq_family.fid = F_ID; + + return w1_register_family(&bq27xxx_battery_hdq_family); +} +module_init(bq27xxx_battery_hdq_init); + +static void __exit bq27xxx_battery_hdq_exit(void) +{ + w1_unregister_family(&bq27xxx_battery_hdq_family); +} +module_exit(bq27xxx_battery_hdq_exit); + +MODULE_AUTHOR("Texas Instruments Ltd"); +MODULE_DESCRIPTION("BQ27xxx battery monitor HDQ/1-wire driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("w1-family-" __stringify(W1_FAMILY_BQ27000)); diff --git a/drivers/power/supply/bq27xxx_battery_i2c.c b/drivers/power/supply/bq27xxx_battery_i2c.c new file mode 100644 index 000000000..0e32efb10 --- /dev/null +++ b/drivers/power/supply/bq27xxx_battery_i2c.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * BQ27xxx battery monitor I2C driver + * + * Copyright (C) 2015 Texas Instruments Incorporated - https://www.ti.com/ + * Andrew F. Davis <afd@ti.com> + */ + +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <asm/unaligned.h> + +#include <linux/power/bq27xxx_battery.h> + +static DEFINE_IDR(battery_id); +static DEFINE_MUTEX(battery_mutex); + +static irqreturn_t bq27xxx_battery_irq_handler_thread(int irq, void *data) +{ + struct bq27xxx_device_info *di = data; + + bq27xxx_battery_update(di); + + return IRQ_HANDLED; +} + +static int bq27xxx_battery_i2c_read(struct bq27xxx_device_info *di, u8 reg, + bool single) +{ + struct i2c_client *client = to_i2c_client(di->dev); + struct i2c_msg msg[2]; + u8 data[2]; + int ret; + + if (!client->adapter) + return -ENODEV; + + msg[0].addr = client->addr; + msg[0].flags = 0; + msg[0].buf = ® + msg[0].len = sizeof(reg); + msg[1].addr = client->addr; + msg[1].flags = I2C_M_RD; + msg[1].buf = data; + if (single) + msg[1].len = 1; + else + msg[1].len = 2; + + ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)); + if (ret < 0) + return ret; + + if (!single) + ret = get_unaligned_le16(data); + else + ret = data[0]; + + return ret; +} + +static int bq27xxx_battery_i2c_write(struct bq27xxx_device_info *di, u8 reg, + int value, bool single) +{ + struct i2c_client *client = to_i2c_client(di->dev); + struct i2c_msg msg; + u8 data[4]; + int ret; + + if (!client->adapter) + return -ENODEV; + + data[0] = reg; + if (single) { + data[1] = (u8) value; + msg.len = 2; + } else { + put_unaligned_le16(value, &data[1]); + msg.len = 3; + } + + msg.buf = data; + msg.addr = client->addr; + msg.flags = 0; + + ret = i2c_transfer(client->adapter, &msg, 1); + if (ret < 0) + return ret; + if (ret != 1) + return -EINVAL; + return 0; +} + +static int bq27xxx_battery_i2c_bulk_read(struct bq27xxx_device_info *di, u8 reg, + u8 *data, int len) +{ + struct i2c_client *client = to_i2c_client(di->dev); + int ret; + + if (!client->adapter) + return -ENODEV; + + ret = i2c_smbus_read_i2c_block_data(client, reg, len, data); + if (ret < 0) + return ret; + if (ret != len) + return -EINVAL; + return 0; +} + +static int bq27xxx_battery_i2c_bulk_write(struct bq27xxx_device_info *di, + u8 reg, u8 *data, int len) +{ + struct i2c_client *client = to_i2c_client(di->dev); + struct i2c_msg msg; + u8 buf[33]; + int ret; + + if (!client->adapter) + return -ENODEV; + + buf[0] = reg; + memcpy(&buf[1], data, len); + + msg.buf = buf; + msg.addr = client->addr; + msg.flags = 0; + msg.len = len + 1; + + ret = i2c_transfer(client->adapter, &msg, 1); + if (ret < 0) + return ret; + if (ret != 1) + return -EINVAL; + return 0; +} + +static int bq27xxx_battery_i2c_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct bq27xxx_device_info *di; + int ret; + char *name; + int num; + + /* Get new ID for the new battery device */ + mutex_lock(&battery_mutex); + num = idr_alloc(&battery_id, client, 0, 0, GFP_KERNEL); + mutex_unlock(&battery_mutex); + if (num < 0) + return num; + + name = devm_kasprintf(&client->dev, GFP_KERNEL, "%s-%d", id->name, num); + if (!name) + goto err_mem; + + di = devm_kzalloc(&client->dev, sizeof(*di), GFP_KERNEL); + if (!di) + goto err_mem; + + di->id = num; + di->dev = &client->dev; + di->chip = id->driver_data; + di->name = name; + + di->bus.read = bq27xxx_battery_i2c_read; + di->bus.write = bq27xxx_battery_i2c_write; + di->bus.read_bulk = bq27xxx_battery_i2c_bulk_read; + di->bus.write_bulk = bq27xxx_battery_i2c_bulk_write; + + ret = bq27xxx_battery_setup(di); + if (ret) + goto err_failed; + + /* Schedule a polling after about 1 min */ + schedule_delayed_work(&di->work, 60 * HZ); + + i2c_set_clientdata(client, di); + + if (client->irq) { + ret = request_threaded_irq(client->irq, + NULL, bq27xxx_battery_irq_handler_thread, + IRQF_ONESHOT, + di->name, di); + if (ret) { + dev_err(&client->dev, + "Unable to register IRQ %d error %d\n", + client->irq, ret); + bq27xxx_battery_teardown(di); + goto err_failed; + } + } + + return 0; + +err_mem: + ret = -ENOMEM; + +err_failed: + mutex_lock(&battery_mutex); + idr_remove(&battery_id, num); + mutex_unlock(&battery_mutex); + + return ret; +} + +static int bq27xxx_battery_i2c_remove(struct i2c_client *client) +{ + struct bq27xxx_device_info *di = i2c_get_clientdata(client); + + free_irq(client->irq, di); + bq27xxx_battery_teardown(di); + + mutex_lock(&battery_mutex); + idr_remove(&battery_id, di->id); + mutex_unlock(&battery_mutex); + + return 0; +} + +static const struct i2c_device_id bq27xxx_i2c_id_table[] = { + { "bq27200", BQ27000 }, + { "bq27210", BQ27010 }, + { "bq27500", BQ2750X }, + { "bq27510", BQ2751X }, + { "bq27520", BQ2752X }, + { "bq27500-1", BQ27500 }, + { "bq27510g1", BQ27510G1 }, + { "bq27510g2", BQ27510G2 }, + { "bq27510g3", BQ27510G3 }, + { "bq27520g1", BQ27520G1 }, + { "bq27520g2", BQ27520G2 }, + { "bq27520g3", BQ27520G3 }, + { "bq27520g4", BQ27520G4 }, + { "bq27521", BQ27521 }, + { "bq27530", BQ27530 }, + { "bq27531", BQ27531 }, + { "bq27541", BQ27541 }, + { "bq27542", BQ27542 }, + { "bq27546", BQ27546 }, + { "bq27742", BQ27742 }, + { "bq27545", BQ27545 }, + { "bq27411", BQ27411 }, + { "bq27421", BQ27421 }, + { "bq27425", BQ27425 }, + { "bq27426", BQ27426 }, + { "bq27441", BQ27441 }, + { "bq27621", BQ27621 }, + { "bq27z561", BQ27Z561 }, + { "bq28z610", BQ28Z610 }, + { "bq34z100", BQ34Z100 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, bq27xxx_i2c_id_table); + +#ifdef CONFIG_OF +static const struct of_device_id bq27xxx_battery_i2c_of_match_table[] = { + { .compatible = "ti,bq27200" }, + { .compatible = "ti,bq27210" }, + { .compatible = "ti,bq27500" }, + { .compatible = "ti,bq27510" }, + { .compatible = "ti,bq27520" }, + { .compatible = "ti,bq27500-1" }, + { .compatible = "ti,bq27510g1" }, + { .compatible = "ti,bq27510g2" }, + { .compatible = "ti,bq27510g3" }, + { .compatible = "ti,bq27520g1" }, + { .compatible = "ti,bq27520g2" }, + { .compatible = "ti,bq27520g3" }, + { .compatible = "ti,bq27520g4" }, + { .compatible = "ti,bq27521" }, + { .compatible = "ti,bq27530" }, + { .compatible = "ti,bq27531" }, + { .compatible = "ti,bq27541" }, + { .compatible = "ti,bq27542" }, + { .compatible = "ti,bq27546" }, + { .compatible = "ti,bq27742" }, + { .compatible = "ti,bq27545" }, + { .compatible = "ti,bq27411" }, + { .compatible = "ti,bq27421" }, + { .compatible = "ti,bq27425" }, + { .compatible = "ti,bq27426" }, + { .compatible = "ti,bq27441" }, + { .compatible = "ti,bq27621" }, + { .compatible = "ti,bq27z561" }, + { .compatible = "ti,bq28z610" }, + { .compatible = "ti,bq34z100" }, + {}, +}; +MODULE_DEVICE_TABLE(of, bq27xxx_battery_i2c_of_match_table); +#endif + +static struct i2c_driver bq27xxx_battery_i2c_driver = { + .driver = { + .name = "bq27xxx-battery", + .of_match_table = of_match_ptr(bq27xxx_battery_i2c_of_match_table), + }, + .probe = bq27xxx_battery_i2c_probe, + .remove = bq27xxx_battery_i2c_remove, + .id_table = bq27xxx_i2c_id_table, +}; +module_i2c_driver(bq27xxx_battery_i2c_driver); + +MODULE_AUTHOR("Andrew F. Davis <afd@ti.com>"); +MODULE_DESCRIPTION("BQ27xxx battery monitor i2c driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/charger-manager.c b/drivers/power/supply/charger-manager.c new file mode 100644 index 000000000..333349275 --- /dev/null +++ b/drivers/power/supply/charger-manager.c @@ -0,0 +1,1773 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2011 Samsung Electronics Co., Ltd. + * MyungJoo Ham <myungjoo.ham@samsung.com> + * + * This driver enables to monitor battery health and control charger + * during suspend-to-mem. + * Charger manager depends on other devices. Register this later than + * the depending devices. + * +**/ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/io.h> +#include <linux/module.h> +#include <linux/irq.h> +#include <linux/interrupt.h> +#include <linux/rtc.h> +#include <linux/slab.h> +#include <linux/workqueue.h> +#include <linux/platform_device.h> +#include <linux/power/charger-manager.h> +#include <linux/regulator/consumer.h> +#include <linux/sysfs.h> +#include <linux/of.h> +#include <linux/thermal.h> + +static struct { + const char *name; + u64 extcon_type; +} extcon_mapping[] = { + /* Current textual representations */ + { "USB", EXTCON_USB }, + { "USB-HOST", EXTCON_USB_HOST }, + { "SDP", EXTCON_CHG_USB_SDP }, + { "DCP", EXTCON_CHG_USB_DCP }, + { "CDP", EXTCON_CHG_USB_CDP }, + { "ACA", EXTCON_CHG_USB_ACA }, + { "FAST-CHARGER", EXTCON_CHG_USB_FAST }, + { "SLOW-CHARGER", EXTCON_CHG_USB_SLOW }, + { "WPT", EXTCON_CHG_WPT }, + { "PD", EXTCON_CHG_USB_PD }, + { "DOCK", EXTCON_DOCK }, + { "JIG", EXTCON_JIG }, + { "MECHANICAL", EXTCON_MECHANICAL }, + /* Deprecated textual representations */ + { "TA", EXTCON_CHG_USB_SDP }, + { "CHARGE-DOWNSTREAM", EXTCON_CHG_USB_CDP }, +}; + +/* + * Default temperature threshold for charging. + * Every temperature units are in tenth of centigrade. + */ +#define CM_DEFAULT_RECHARGE_TEMP_DIFF 50 +#define CM_DEFAULT_CHARGE_TEMP_MAX 500 + +/* + * Regard CM_JIFFIES_SMALL jiffies is small enough to ignore for + * delayed works so that we can run delayed works with CM_JIFFIES_SMALL + * without any delays. + */ +#define CM_JIFFIES_SMALL (2) + +/* If y is valid (> 0) and smaller than x, do x = y */ +#define CM_MIN_VALID(x, y) x = (((y > 0) && ((x) > (y))) ? (y) : (x)) + +/* + * Regard CM_RTC_SMALL (sec) is small enough to ignore error in invoking + * rtc alarm. It should be 2 or larger + */ +#define CM_RTC_SMALL (2) + +static LIST_HEAD(cm_list); +static DEFINE_MUTEX(cm_list_mtx); + +/* About in-suspend (suspend-again) monitoring */ +static struct alarm *cm_timer; + +static bool cm_suspended; +static bool cm_timer_set; +static unsigned long cm_suspend_duration_ms; + +/* About normal (not suspended) monitoring */ +static unsigned long polling_jiffy = ULONG_MAX; /* ULONG_MAX: no polling */ +static unsigned long next_polling; /* Next appointed polling time */ +static struct workqueue_struct *cm_wq; /* init at driver add */ +static struct delayed_work cm_monitor_work; /* init at driver add */ + +/** + * is_batt_present - See if the battery presents in place. + * @cm: the Charger Manager representing the battery. + */ +static bool is_batt_present(struct charger_manager *cm) +{ + union power_supply_propval val; + struct power_supply *psy; + bool present = false; + int i, ret; + + switch (cm->desc->battery_present) { + case CM_BATTERY_PRESENT: + present = true; + break; + case CM_NO_BATTERY: + break; + case CM_FUEL_GAUGE: + psy = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!psy) + break; + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_PRESENT, + &val); + if (ret == 0 && val.intval) + present = true; + power_supply_put(psy); + break; + case CM_CHARGER_STAT: + for (i = 0; cm->desc->psy_charger_stat[i]; i++) { + psy = power_supply_get_by_name( + cm->desc->psy_charger_stat[i]); + if (!psy) { + dev_err(cm->dev, "Cannot find power supply \"%s\"\n", + cm->desc->psy_charger_stat[i]); + continue; + } + + ret = power_supply_get_property(psy, + POWER_SUPPLY_PROP_PRESENT, &val); + power_supply_put(psy); + if (ret == 0 && val.intval) { + present = true; + break; + } + } + break; + } + + return present; +} + +/** + * is_ext_pwr_online - See if an external power source is attached to charge + * @cm: the Charger Manager representing the battery. + * + * Returns true if at least one of the chargers of the battery has an external + * power source attached to charge the battery regardless of whether it is + * actually charging or not. + */ +static bool is_ext_pwr_online(struct charger_manager *cm) +{ + union power_supply_propval val; + struct power_supply *psy; + bool online = false; + int i, ret; + + /* If at least one of them has one, it's yes. */ + for (i = 0; cm->desc->psy_charger_stat[i]; i++) { + psy = power_supply_get_by_name(cm->desc->psy_charger_stat[i]); + if (!psy) { + dev_err(cm->dev, "Cannot find power supply \"%s\"\n", + cm->desc->psy_charger_stat[i]); + continue; + } + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_ONLINE, + &val); + power_supply_put(psy); + if (ret == 0 && val.intval) { + online = true; + break; + } + } + + return online; +} + +/** + * get_batt_uV - Get the voltage level of the battery + * @cm: the Charger Manager representing the battery. + * @uV: the voltage level returned. + * + * Returns 0 if there is no error. + * Returns a negative value on error. + */ +static int get_batt_uV(struct charger_manager *cm, int *uV) +{ + union power_supply_propval val; + struct power_supply *fuel_gauge; + int ret; + + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) + return -ENODEV; + + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_VOLTAGE_NOW, &val); + power_supply_put(fuel_gauge); + if (ret) + return ret; + + *uV = val.intval; + return 0; +} + +/** + * is_charging - Returns true if the battery is being charged. + * @cm: the Charger Manager representing the battery. + */ +static bool is_charging(struct charger_manager *cm) +{ + int i, ret; + bool charging = false; + struct power_supply *psy; + union power_supply_propval val; + + /* If there is no battery, it cannot be charged */ + if (!is_batt_present(cm)) + return false; + + /* If at least one of the charger is charging, return yes */ + for (i = 0; cm->desc->psy_charger_stat[i]; i++) { + /* 1. The charger sholuld not be DISABLED */ + if (cm->emergency_stop) + continue; + if (!cm->charger_enabled) + continue; + + psy = power_supply_get_by_name(cm->desc->psy_charger_stat[i]); + if (!psy) { + dev_err(cm->dev, "Cannot find power supply \"%s\"\n", + cm->desc->psy_charger_stat[i]); + continue; + } + + /* 2. The charger should be online (ext-power) */ + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_ONLINE, + &val); + if (ret) { + dev_warn(cm->dev, "Cannot read ONLINE value from %s\n", + cm->desc->psy_charger_stat[i]); + power_supply_put(psy); + continue; + } + if (val.intval == 0) { + power_supply_put(psy); + continue; + } + + /* + * 3. The charger should not be FULL, DISCHARGING, + * or NOT_CHARGING. + */ + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_STATUS, + &val); + power_supply_put(psy); + if (ret) { + dev_warn(cm->dev, "Cannot read STATUS value from %s\n", + cm->desc->psy_charger_stat[i]); + continue; + } + if (val.intval == POWER_SUPPLY_STATUS_FULL || + val.intval == POWER_SUPPLY_STATUS_DISCHARGING || + val.intval == POWER_SUPPLY_STATUS_NOT_CHARGING) + continue; + + /* Then, this is charging. */ + charging = true; + break; + } + + return charging; +} + +/** + * is_full_charged - Returns true if the battery is fully charged. + * @cm: the Charger Manager representing the battery. + */ +static bool is_full_charged(struct charger_manager *cm) +{ + struct charger_desc *desc = cm->desc; + union power_supply_propval val; + struct power_supply *fuel_gauge; + bool is_full = false; + int ret = 0; + int uV; + + /* If there is no battery, it cannot be charged */ + if (!is_batt_present(cm)) + return false; + + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) + return false; + + /* Full, if it's over the fullbatt voltage */ + if (desc->fullbatt_uV > 0) { + ret = get_batt_uV(cm, &uV); + if (!ret) { + /* Battery is already full, checks voltage drop. */ + if (cm->battery_status == POWER_SUPPLY_STATUS_FULL + && desc->fullbatt_vchkdrop_uV) + uV += desc->fullbatt_vchkdrop_uV; + if (uV >= desc->fullbatt_uV) + return true; + } + } + + if (desc->fullbatt_full_capacity > 0) { + val.intval = 0; + + /* Not full if capacity of fuel gauge isn't full */ + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CHARGE_FULL, &val); + if (!ret && val.intval > desc->fullbatt_full_capacity) { + is_full = true; + goto out; + } + } + + /* Full, if the capacity is more than fullbatt_soc */ + if (desc->fullbatt_soc > 0) { + val.intval = 0; + + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CAPACITY, &val); + if (!ret && val.intval >= desc->fullbatt_soc) { + is_full = true; + goto out; + } + } + +out: + power_supply_put(fuel_gauge); + return is_full; +} + +/** + * is_polling_required - Return true if need to continue polling for this CM. + * @cm: the Charger Manager representing the battery. + */ +static bool is_polling_required(struct charger_manager *cm) +{ + switch (cm->desc->polling_mode) { + case CM_POLL_DISABLE: + return false; + case CM_POLL_ALWAYS: + return true; + case CM_POLL_EXTERNAL_POWER_ONLY: + return is_ext_pwr_online(cm); + case CM_POLL_CHARGING_ONLY: + return is_charging(cm); + default: + dev_warn(cm->dev, "Incorrect polling_mode (%d)\n", + cm->desc->polling_mode); + } + + return false; +} + +/** + * try_charger_enable - Enable/Disable chargers altogether + * @cm: the Charger Manager representing the battery. + * @enable: true: enable / false: disable + * + * Note that Charger Manager keeps the charger enabled regardless whether + * the charger is charging or not (because battery is full or no external + * power source exists) except when CM needs to disable chargers forcibly + * because of emergency causes; when the battery is overheated or too cold. + */ +static int try_charger_enable(struct charger_manager *cm, bool enable) +{ + int err = 0, i; + struct charger_desc *desc = cm->desc; + + /* Ignore if it's redundant command */ + if (enable == cm->charger_enabled) + return 0; + + if (enable) { + if (cm->emergency_stop) + return -EAGAIN; + + /* + * Save start time of charging to limit + * maximum possible charging time. + */ + cm->charging_start_time = ktime_to_ms(ktime_get()); + cm->charging_end_time = 0; + + for (i = 0 ; i < desc->num_charger_regulators ; i++) { + if (desc->charger_regulators[i].externally_control) + continue; + + err = regulator_enable(desc->charger_regulators[i].consumer); + if (err < 0) { + dev_warn(cm->dev, "Cannot enable %s regulator\n", + desc->charger_regulators[i].regulator_name); + } + } + } else { + /* + * Save end time of charging to maintain fully charged state + * of battery after full-batt. + */ + cm->charging_start_time = 0; + cm->charging_end_time = ktime_to_ms(ktime_get()); + + for (i = 0 ; i < desc->num_charger_regulators ; i++) { + if (desc->charger_regulators[i].externally_control) + continue; + + err = regulator_disable(desc->charger_regulators[i].consumer); + if (err < 0) { + dev_warn(cm->dev, "Cannot disable %s regulator\n", + desc->charger_regulators[i].regulator_name); + } + } + + /* + * Abnormal battery state - Stop charging forcibly, + * even if charger was enabled at the other places + */ + for (i = 0; i < desc->num_charger_regulators; i++) { + if (regulator_is_enabled( + desc->charger_regulators[i].consumer)) { + regulator_force_disable( + desc->charger_regulators[i].consumer); + dev_warn(cm->dev, "Disable regulator(%s) forcibly\n", + desc->charger_regulators[i].regulator_name); + } + } + } + + if (!err) + cm->charger_enabled = enable; + + return err; +} + +/** + * check_charging_duration - Monitor charging/discharging duration + * @cm: the Charger Manager representing the battery. + * + * If whole charging duration exceed 'charging_max_duration_ms', + * cm stop charging to prevent overcharge/overheat. If discharging + * duration exceed 'discharging _max_duration_ms', charger cable is + * attached, after full-batt, cm start charging to maintain fully + * charged state for battery. + */ +static int check_charging_duration(struct charger_manager *cm) +{ + struct charger_desc *desc = cm->desc; + u64 curr = ktime_to_ms(ktime_get()); + u64 duration; + int ret = false; + + if (!desc->charging_max_duration_ms && + !desc->discharging_max_duration_ms) + return ret; + + if (cm->charger_enabled) { + duration = curr - cm->charging_start_time; + + if (duration > desc->charging_max_duration_ms) { + dev_info(cm->dev, "Charging duration exceed %ums\n", + desc->charging_max_duration_ms); + ret = true; + } + } else if (cm->battery_status == POWER_SUPPLY_STATUS_NOT_CHARGING) { + duration = curr - cm->charging_end_time; + + if (duration > desc->discharging_max_duration_ms) { + dev_info(cm->dev, "Discharging duration exceed %ums\n", + desc->discharging_max_duration_ms); + ret = true; + } + } + + return ret; +} + +static int cm_get_battery_temperature_by_psy(struct charger_manager *cm, + int *temp) +{ + struct power_supply *fuel_gauge; + int ret; + + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) + return -ENODEV; + + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_TEMP, + (union power_supply_propval *)temp); + power_supply_put(fuel_gauge); + + return ret; +} + +static int cm_get_battery_temperature(struct charger_manager *cm, + int *temp) +{ + int ret; + + if (!cm->desc->measure_battery_temp) + return -ENODEV; + +#ifdef CONFIG_THERMAL + if (cm->tzd_batt) { + ret = thermal_zone_get_temp(cm->tzd_batt, temp); + if (!ret) + /* Calibrate temperature unit */ + *temp /= 100; + } else +#endif + { + /* if-else continued from CONFIG_THERMAL */ + ret = cm_get_battery_temperature_by_psy(cm, temp); + } + + return ret; +} + +static int cm_check_thermal_status(struct charger_manager *cm) +{ + struct charger_desc *desc = cm->desc; + int temp, upper_limit, lower_limit; + int ret = 0; + + ret = cm_get_battery_temperature(cm, &temp); + if (ret) { + /* FIXME: + * No information of battery temperature might + * occur hazardous result. We have to handle it + * depending on battery type. + */ + dev_err(cm->dev, "Failed to get battery temperature\n"); + return 0; + } + + upper_limit = desc->temp_max; + lower_limit = desc->temp_min; + + if (cm->emergency_stop) { + upper_limit -= desc->temp_diff; + lower_limit += desc->temp_diff; + } + + if (temp > upper_limit) + ret = CM_BATT_OVERHEAT; + else if (temp < lower_limit) + ret = CM_BATT_COLD; + else + ret = CM_BATT_OK; + + cm->emergency_stop = ret; + + return ret; +} + +/** + * cm_get_target_status - Check current status and get next target status. + * @cm: the Charger Manager representing the battery. + */ +static int cm_get_target_status(struct charger_manager *cm) +{ + if (!is_ext_pwr_online(cm)) + return POWER_SUPPLY_STATUS_DISCHARGING; + + if (cm_check_thermal_status(cm)) { + /* Check if discharging duration exeeds limit. */ + if (check_charging_duration(cm)) + goto charging_ok; + return POWER_SUPPLY_STATUS_NOT_CHARGING; + } + + switch (cm->battery_status) { + case POWER_SUPPLY_STATUS_CHARGING: + /* Check if charging duration exeeds limit. */ + if (check_charging_duration(cm)) + return POWER_SUPPLY_STATUS_FULL; + fallthrough; + case POWER_SUPPLY_STATUS_FULL: + if (is_full_charged(cm)) + return POWER_SUPPLY_STATUS_FULL; + fallthrough; + default: + break; + } + +charging_ok: + /* Charging is allowed. */ + return POWER_SUPPLY_STATUS_CHARGING; +} + +/** + * _cm_monitor - Monitor the temperature and return true for exceptions. + * @cm: the Charger Manager representing the battery. + * + * Returns true if there is an event to notify for the battery. + * (True if the status of "emergency_stop" changes) + */ +static bool _cm_monitor(struct charger_manager *cm) +{ + int target; + + target = cm_get_target_status(cm); + + try_charger_enable(cm, (target == POWER_SUPPLY_STATUS_CHARGING)); + + if (cm->battery_status != target) { + cm->battery_status = target; + power_supply_changed(cm->charger_psy); + } + + return (cm->battery_status == POWER_SUPPLY_STATUS_NOT_CHARGING); +} + +/** + * cm_monitor - Monitor every battery. + * + * Returns true if there is an event to notify from any of the batteries. + * (True if the status of "emergency_stop" changes) + */ +static bool cm_monitor(void) +{ + bool stop = false; + struct charger_manager *cm; + + mutex_lock(&cm_list_mtx); + + list_for_each_entry(cm, &cm_list, entry) { + if (_cm_monitor(cm)) + stop = true; + } + + mutex_unlock(&cm_list_mtx); + + return stop; +} + +/** + * _setup_polling - Setup the next instance of polling. + * @work: work_struct of the function _setup_polling. + */ +static void _setup_polling(struct work_struct *work) +{ + unsigned long min = ULONG_MAX; + struct charger_manager *cm; + bool keep_polling = false; + unsigned long _next_polling; + + mutex_lock(&cm_list_mtx); + + list_for_each_entry(cm, &cm_list, entry) { + if (is_polling_required(cm) && cm->desc->polling_interval_ms) { + keep_polling = true; + + if (min > cm->desc->polling_interval_ms) + min = cm->desc->polling_interval_ms; + } + } + + polling_jiffy = msecs_to_jiffies(min); + if (polling_jiffy <= CM_JIFFIES_SMALL) + polling_jiffy = CM_JIFFIES_SMALL + 1; + + if (!keep_polling) + polling_jiffy = ULONG_MAX; + if (polling_jiffy == ULONG_MAX) + goto out; + + WARN(cm_wq == NULL, "charger-manager: workqueue not initialized" + ". try it later. %s\n", __func__); + + /* + * Use mod_delayed_work() iff the next polling interval should + * occur before the currently scheduled one. If @cm_monitor_work + * isn't active, the end result is the same, so no need to worry + * about stale @next_polling. + */ + _next_polling = jiffies + polling_jiffy; + + if (time_before(_next_polling, next_polling)) { + mod_delayed_work(cm_wq, &cm_monitor_work, polling_jiffy); + next_polling = _next_polling; + } else { + if (queue_delayed_work(cm_wq, &cm_monitor_work, polling_jiffy)) + next_polling = _next_polling; + } +out: + mutex_unlock(&cm_list_mtx); +} +static DECLARE_WORK(setup_polling, _setup_polling); + +/** + * cm_monitor_poller - The Monitor / Poller. + * @work: work_struct of the function cm_monitor_poller + * + * During non-suspended state, cm_monitor_poller is used to poll and monitor + * the batteries. + */ +static void cm_monitor_poller(struct work_struct *work) +{ + cm_monitor(); + schedule_work(&setup_polling); +} + +static int charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct charger_manager *cm = power_supply_get_drvdata(psy); + struct charger_desc *desc = cm->desc; + struct power_supply *fuel_gauge = NULL; + int ret = 0; + int uV; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = cm->battery_status; + break; + case POWER_SUPPLY_PROP_HEALTH: + if (cm->emergency_stop > 0) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (cm->emergency_stop < 0) + val->intval = POWER_SUPPLY_HEALTH_COLD; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_PRESENT: + if (is_batt_present(cm)) + val->intval = 1; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = get_batt_uV(cm, &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) { + ret = -ENODEV; + break; + } + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CURRENT_NOW, val); + break; + case POWER_SUPPLY_PROP_TEMP: + return cm_get_battery_temperature(cm, &val->intval); + case POWER_SUPPLY_PROP_CAPACITY: + if (!is_batt_present(cm)) { + /* There is no battery. Assume 100% */ + val->intval = 100; + break; + } + + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) { + ret = -ENODEV; + break; + } + + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CAPACITY, val); + if (ret) + break; + + if (val->intval > 100) { + val->intval = 100; + break; + } + if (val->intval < 0) + val->intval = 0; + + /* Do not adjust SOC when charging: voltage is overrated */ + if (is_charging(cm)) + break; + + /* + * If the capacity value is inconsistent, calibrate it base on + * the battery voltage values and the thresholds given as desc + */ + ret = get_batt_uV(cm, &uV); + if (ret) { + /* Voltage information not available. No calibration */ + ret = 0; + break; + } + + if (desc->fullbatt_uV > 0 && uV >= desc->fullbatt_uV && + !is_charging(cm)) { + val->intval = 100; + break; + } + + break; + case POWER_SUPPLY_PROP_ONLINE: + if (is_ext_pwr_online(cm)) + val->intval = 1; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_NOW: + fuel_gauge = power_supply_get_by_name(cm->desc->psy_fuel_gauge); + if (!fuel_gauge) { + ret = -ENODEV; + break; + } + ret = power_supply_get_property(fuel_gauge, psp, val); + break; + default: + return -EINVAL; + } + if (fuel_gauge) + power_supply_put(fuel_gauge); + return ret; +} + +#define NUM_CHARGER_PSY_OPTIONAL (4) +static enum power_supply_property default_charger_props[] = { + /* Guaranteed to provide */ + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_ONLINE, + /* + * Optional properties are: + * POWER_SUPPLY_PROP_CHARGE_FULL, + * POWER_SUPPLY_PROP_CHARGE_NOW, + * POWER_SUPPLY_PROP_CURRENT_NOW, + * POWER_SUPPLY_PROP_TEMP, + */ +}; + +static const struct power_supply_desc psy_default = { + .name = "battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = default_charger_props, + .num_properties = ARRAY_SIZE(default_charger_props), + .get_property = charger_get_property, + .no_thermal = true, +}; + +/** + * cm_setup_timer - For in-suspend monitoring setup wakeup alarm + * for suspend_again. + * + * Returns true if the alarm is set for Charger Manager to use. + * Returns false if + * cm_setup_timer fails to set an alarm, + * cm_setup_timer does not need to set an alarm for Charger Manager, + * or an alarm previously configured is to be used. + */ +static bool cm_setup_timer(void) +{ + struct charger_manager *cm; + unsigned int wakeup_ms = UINT_MAX; + int timer_req = 0; + + if (time_after(next_polling, jiffies)) + CM_MIN_VALID(wakeup_ms, + jiffies_to_msecs(next_polling - jiffies)); + + mutex_lock(&cm_list_mtx); + list_for_each_entry(cm, &cm_list, entry) { + /* Skip if polling is not required for this CM */ + if (!is_polling_required(cm) && !cm->emergency_stop) + continue; + timer_req++; + if (cm->desc->polling_interval_ms == 0) + continue; + CM_MIN_VALID(wakeup_ms, cm->desc->polling_interval_ms); + } + mutex_unlock(&cm_list_mtx); + + if (timer_req && cm_timer) { + ktime_t now, add; + + /* + * Set alarm with the polling interval (wakeup_ms) + * The alarm time should be NOW + CM_RTC_SMALL or later. + */ + if (wakeup_ms == UINT_MAX || + wakeup_ms < CM_RTC_SMALL * MSEC_PER_SEC) + wakeup_ms = 2 * CM_RTC_SMALL * MSEC_PER_SEC; + + pr_info("Charger Manager wakeup timer: %u ms\n", wakeup_ms); + + now = ktime_get_boottime(); + add = ktime_set(wakeup_ms / MSEC_PER_SEC, + (wakeup_ms % MSEC_PER_SEC) * NSEC_PER_MSEC); + alarm_start(cm_timer, ktime_add(now, add)); + + cm_suspend_duration_ms = wakeup_ms; + + return true; + } + return false; +} + +/** + * charger_extcon_work - enable/diable charger according to the state + * of charger cable + * + * @work: work_struct of the function charger_extcon_work. + */ +static void charger_extcon_work(struct work_struct *work) +{ + struct charger_cable *cable = + container_of(work, struct charger_cable, wq); + int ret; + + if (cable->attached && cable->min_uA != 0 && cable->max_uA != 0) { + ret = regulator_set_current_limit(cable->charger->consumer, + cable->min_uA, cable->max_uA); + if (ret < 0) { + pr_err("Cannot set current limit of %s (%s)\n", + cable->charger->regulator_name, cable->name); + return; + } + + pr_info("Set current limit of %s : %duA ~ %duA\n", + cable->charger->regulator_name, + cable->min_uA, cable->max_uA); + } + + cancel_delayed_work(&cm_monitor_work); + queue_delayed_work(cm_wq, &cm_monitor_work, 0); +} + +/** + * charger_extcon_notifier - receive the state of charger cable + * when registered cable is attached or detached. + * + * @self: the notifier block of the charger_extcon_notifier. + * @event: the cable state. + * @ptr: the data pointer of notifier block. + */ +static int charger_extcon_notifier(struct notifier_block *self, + unsigned long event, void *ptr) +{ + struct charger_cable *cable = + container_of(self, struct charger_cable, nb); + + /* + * The newly state of charger cable. + * If cable is attached, cable->attached is true. + */ + cable->attached = event; + + /* + * Setup work for controlling charger(regulator) + * according to charger cable. + */ + schedule_work(&cable->wq); + + return NOTIFY_DONE; +} + +/** + * charger_extcon_init - register external connector to use it + * as the charger cable + * + * @cm: the Charger Manager representing the battery. + * @cable: the Charger cable representing the external connector. + */ +static int charger_extcon_init(struct charger_manager *cm, + struct charger_cable *cable) +{ + int ret, i; + u64 extcon_type = EXTCON_NONE; + + /* + * Charger manager use Extcon framework to identify + * the charger cable among various external connector + * cable (e.g., TA, USB, MHL, Dock). + */ + INIT_WORK(&cable->wq, charger_extcon_work); + cable->nb.notifier_call = charger_extcon_notifier; + + cable->extcon_dev = extcon_get_extcon_dev(cable->extcon_name); + if (IS_ERR_OR_NULL(cable->extcon_dev)) { + pr_err("Cannot find extcon_dev for %s (cable: %s)\n", + cable->extcon_name, cable->name); + if (cable->extcon_dev == NULL) + return -EPROBE_DEFER; + else + return PTR_ERR(cable->extcon_dev); + } + + for (i = 0; i < ARRAY_SIZE(extcon_mapping); i++) { + if (!strcmp(cable->name, extcon_mapping[i].name)) { + extcon_type = extcon_mapping[i].extcon_type; + break; + } + } + if (extcon_type == EXTCON_NONE) { + pr_err("Cannot find cable for type %s", cable->name); + return -EINVAL; + } + + cable->extcon_type = extcon_type; + + ret = devm_extcon_register_notifier(cm->dev, cable->extcon_dev, + cable->extcon_type, &cable->nb); + if (ret < 0) { + pr_err("Cannot register extcon_dev for %s (cable: %s)\n", + cable->extcon_name, cable->name); + return ret; + } + + return 0; +} + +/** + * charger_manager_register_extcon - Register extcon device to receive state + * of charger cable. + * @cm: the Charger Manager representing the battery. + * + * This function support EXTCON(External Connector) subsystem to detect the + * state of charger cables for enabling or disabling charger(regulator) and + * select the charger cable for charging among a number of external cable + * according to policy of H/W board. + */ +static int charger_manager_register_extcon(struct charger_manager *cm) +{ + struct charger_desc *desc = cm->desc; + struct charger_regulator *charger; + unsigned long event; + int ret; + int i; + int j; + + for (i = 0; i < desc->num_charger_regulators; i++) { + charger = &desc->charger_regulators[i]; + + charger->consumer = regulator_get(cm->dev, + charger->regulator_name); + if (IS_ERR(charger->consumer)) { + dev_err(cm->dev, "Cannot find charger(%s)\n", + charger->regulator_name); + return PTR_ERR(charger->consumer); + } + charger->cm = cm; + + for (j = 0; j < charger->num_cables; j++) { + struct charger_cable *cable = &charger->cables[j]; + + ret = charger_extcon_init(cm, cable); + if (ret < 0) { + dev_err(cm->dev, "Cannot initialize charger(%s)\n", + charger->regulator_name); + return ret; + } + cable->charger = charger; + cable->cm = cm; + + event = extcon_get_state(cable->extcon_dev, + cable->extcon_type); + charger_extcon_notifier(&cable->nb, + event, NULL); + } + } + + return 0; +} + +/* help function of sysfs node to control charger(regulator) */ +static ssize_t charger_name_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct charger_regulator *charger + = container_of(attr, struct charger_regulator, attr_name); + + return sprintf(buf, "%s\n", charger->regulator_name); +} + +static ssize_t charger_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct charger_regulator *charger + = container_of(attr, struct charger_regulator, attr_state); + int state = 0; + + if (!charger->externally_control) + state = regulator_is_enabled(charger->consumer); + + return sprintf(buf, "%s\n", state ? "enabled" : "disabled"); +} + +static ssize_t charger_externally_control_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct charger_regulator *charger = container_of(attr, + struct charger_regulator, attr_externally_control); + + return sprintf(buf, "%d\n", charger->externally_control); +} + +static ssize_t charger_externally_control_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct charger_regulator *charger + = container_of(attr, struct charger_regulator, + attr_externally_control); + struct charger_manager *cm = charger->cm; + struct charger_desc *desc = cm->desc; + int i; + int ret; + int externally_control; + int chargers_externally_control = 1; + + ret = sscanf(buf, "%d", &externally_control); + if (ret == 0) { + ret = -EINVAL; + return ret; + } + + if (!externally_control) { + charger->externally_control = 0; + return count; + } + + for (i = 0; i < desc->num_charger_regulators; i++) { + if (&desc->charger_regulators[i] != charger && + !desc->charger_regulators[i].externally_control) { + /* + * At least, one charger is controlled by + * charger-manager + */ + chargers_externally_control = 0; + break; + } + } + + if (!chargers_externally_control) { + if (cm->charger_enabled) { + try_charger_enable(charger->cm, false); + charger->externally_control = externally_control; + try_charger_enable(charger->cm, true); + } else { + charger->externally_control = externally_control; + } + } else { + dev_warn(cm->dev, + "'%s' regulator should be controlled in charger-manager because charger-manager must need at least one charger for charging\n", + charger->regulator_name); + } + + return count; +} + +/** + * charger_manager_prepare_sysfs - Prepare sysfs entry for each charger + * @cm: the Charger Manager representing the battery. + * + * This function add sysfs entry for charger(regulator) to control charger from + * user-space. If some development board use one more chargers for charging + * but only need one charger on specific case which is dependent on user + * scenario or hardware restrictions, the user enter 1 or 0(zero) to '/sys/ + * class/power_supply/battery/charger.[index]/externally_control'. For example, + * if user enter 1 to 'sys/class/power_supply/battery/charger.[index]/ + * externally_control, this charger isn't controlled from charger-manager and + * always stay off state of regulator. + */ +static int charger_manager_prepare_sysfs(struct charger_manager *cm) +{ + struct charger_desc *desc = cm->desc; + struct charger_regulator *charger; + int chargers_externally_control = 1; + char *name; + int i; + + /* Create sysfs entry to control charger(regulator) */ + for (i = 0; i < desc->num_charger_regulators; i++) { + charger = &desc->charger_regulators[i]; + + name = devm_kasprintf(cm->dev, GFP_KERNEL, "charger.%d", i); + if (!name) + return -ENOMEM; + + charger->attrs[0] = &charger->attr_name.attr; + charger->attrs[1] = &charger->attr_state.attr; + charger->attrs[2] = &charger->attr_externally_control.attr; + charger->attrs[3] = NULL; + + charger->attr_grp.name = name; + charger->attr_grp.attrs = charger->attrs; + desc->sysfs_groups[i] = &charger->attr_grp; + + sysfs_attr_init(&charger->attr_name.attr); + charger->attr_name.attr.name = "name"; + charger->attr_name.attr.mode = 0444; + charger->attr_name.show = charger_name_show; + + sysfs_attr_init(&charger->attr_state.attr); + charger->attr_state.attr.name = "state"; + charger->attr_state.attr.mode = 0444; + charger->attr_state.show = charger_state_show; + + sysfs_attr_init(&charger->attr_externally_control.attr); + charger->attr_externally_control.attr.name + = "externally_control"; + charger->attr_externally_control.attr.mode = 0644; + charger->attr_externally_control.show + = charger_externally_control_show; + charger->attr_externally_control.store + = charger_externally_control_store; + + if (!desc->charger_regulators[i].externally_control || + !chargers_externally_control) + chargers_externally_control = 0; + + dev_info(cm->dev, "'%s' regulator's externally_control is %d\n", + charger->regulator_name, charger->externally_control); + } + + if (chargers_externally_control) { + dev_err(cm->dev, "Cannot register regulator because charger-manager must need at least one charger for charging battery\n"); + return -EINVAL; + } + + return 0; +} + +static int cm_init_thermal_data(struct charger_manager *cm, + struct power_supply *fuel_gauge, + enum power_supply_property *properties, + size_t *num_properties) +{ + struct charger_desc *desc = cm->desc; + union power_supply_propval val; + int ret; + + /* Verify whether fuel gauge provides battery temperature */ + ret = power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_TEMP, &val); + + if (!ret) { + properties[*num_properties] = POWER_SUPPLY_PROP_TEMP; + (*num_properties)++; + cm->desc->measure_battery_temp = true; + } +#ifdef CONFIG_THERMAL + if (ret && desc->thermal_zone) { + cm->tzd_batt = + thermal_zone_get_zone_by_name(desc->thermal_zone); + if (IS_ERR(cm->tzd_batt)) + return PTR_ERR(cm->tzd_batt); + + /* Use external thermometer */ + properties[*num_properties] = POWER_SUPPLY_PROP_TEMP; + (*num_properties)++; + cm->desc->measure_battery_temp = true; + ret = 0; + } +#endif + if (cm->desc->measure_battery_temp) { + /* NOTICE : Default allowable minimum charge temperature is 0 */ + if (!desc->temp_max) + desc->temp_max = CM_DEFAULT_CHARGE_TEMP_MAX; + if (!desc->temp_diff) + desc->temp_diff = CM_DEFAULT_RECHARGE_TEMP_DIFF; + } + + return ret; +} + +static const struct of_device_id charger_manager_match[] = { + { + .compatible = "charger-manager", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, charger_manager_match); + +static struct charger_desc *of_cm_parse_desc(struct device *dev) +{ + struct charger_desc *desc; + struct device_node *np = dev->of_node; + u32 poll_mode = CM_POLL_DISABLE; + u32 battery_stat = CM_NO_BATTERY; + int num_chgs = 0; + + desc = devm_kzalloc(dev, sizeof(*desc), GFP_KERNEL); + if (!desc) + return ERR_PTR(-ENOMEM); + + of_property_read_string(np, "cm-name", &desc->psy_name); + + of_property_read_u32(np, "cm-poll-mode", &poll_mode); + desc->polling_mode = poll_mode; + + of_property_read_u32(np, "cm-poll-interval", + &desc->polling_interval_ms); + + of_property_read_u32(np, "cm-fullbatt-vchkdrop-volt", + &desc->fullbatt_vchkdrop_uV); + of_property_read_u32(np, "cm-fullbatt-voltage", &desc->fullbatt_uV); + of_property_read_u32(np, "cm-fullbatt-soc", &desc->fullbatt_soc); + of_property_read_u32(np, "cm-fullbatt-capacity", + &desc->fullbatt_full_capacity); + + of_property_read_u32(np, "cm-battery-stat", &battery_stat); + desc->battery_present = battery_stat; + + /* chargers */ + num_chgs = of_property_count_strings(np, "cm-chargers"); + if (num_chgs > 0) { + int i; + + /* Allocate empty bin at the tail of array */ + desc->psy_charger_stat = devm_kcalloc(dev, + num_chgs + 1, + sizeof(char *), + GFP_KERNEL); + if (!desc->psy_charger_stat) + return ERR_PTR(-ENOMEM); + + for (i = 0; i < num_chgs; i++) + of_property_read_string_index(np, "cm-chargers", + i, &desc->psy_charger_stat[i]); + } + + of_property_read_string(np, "cm-fuel-gauge", &desc->psy_fuel_gauge); + + of_property_read_string(np, "cm-thermal-zone", &desc->thermal_zone); + + of_property_read_u32(np, "cm-battery-cold", &desc->temp_min); + if (of_get_property(np, "cm-battery-cold-in-minus", NULL)) + desc->temp_min *= -1; + of_property_read_u32(np, "cm-battery-hot", &desc->temp_max); + of_property_read_u32(np, "cm-battery-temp-diff", &desc->temp_diff); + + of_property_read_u32(np, "cm-charging-max", + &desc->charging_max_duration_ms); + of_property_read_u32(np, "cm-discharging-max", + &desc->discharging_max_duration_ms); + + /* battery charger regulators */ + desc->num_charger_regulators = of_get_child_count(np); + if (desc->num_charger_regulators) { + struct charger_regulator *chg_regs; + struct device_node *child; + + chg_regs = devm_kcalloc(dev, + desc->num_charger_regulators, + sizeof(*chg_regs), + GFP_KERNEL); + if (!chg_regs) + return ERR_PTR(-ENOMEM); + + desc->charger_regulators = chg_regs; + + desc->sysfs_groups = devm_kcalloc(dev, + desc->num_charger_regulators + 1, + sizeof(*desc->sysfs_groups), + GFP_KERNEL); + if (!desc->sysfs_groups) + return ERR_PTR(-ENOMEM); + + for_each_child_of_node(np, child) { + struct charger_cable *cables; + struct device_node *_child; + + of_property_read_string(child, "cm-regulator-name", + &chg_regs->regulator_name); + + /* charger cables */ + chg_regs->num_cables = of_get_child_count(child); + if (chg_regs->num_cables) { + cables = devm_kcalloc(dev, + chg_regs->num_cables, + sizeof(*cables), + GFP_KERNEL); + if (!cables) { + of_node_put(child); + return ERR_PTR(-ENOMEM); + } + + chg_regs->cables = cables; + + for_each_child_of_node(child, _child) { + of_property_read_string(_child, + "cm-cable-name", &cables->name); + of_property_read_string(_child, + "cm-cable-extcon", + &cables->extcon_name); + of_property_read_u32(_child, + "cm-cable-min", + &cables->min_uA); + of_property_read_u32(_child, + "cm-cable-max", + &cables->max_uA); + cables++; + } + } + chg_regs++; + } + } + return desc; +} + +static inline struct charger_desc *cm_get_drv_data(struct platform_device *pdev) +{ + if (pdev->dev.of_node) + return of_cm_parse_desc(&pdev->dev); + return dev_get_platdata(&pdev->dev); +} + +static enum alarmtimer_restart cm_timer_func(struct alarm *alarm, ktime_t now) +{ + cm_timer_set = false; + return ALARMTIMER_NORESTART; +} + +static int charger_manager_probe(struct platform_device *pdev) +{ + struct charger_desc *desc = cm_get_drv_data(pdev); + struct charger_manager *cm; + int ret, i = 0; + union power_supply_propval val; + struct power_supply *fuel_gauge; + enum power_supply_property *properties; + size_t num_properties; + struct power_supply_config psy_cfg = {}; + + if (IS_ERR(desc)) { + dev_err(&pdev->dev, "No platform data (desc) found\n"); + return PTR_ERR(desc); + } + + cm = devm_kzalloc(&pdev->dev, sizeof(*cm), GFP_KERNEL); + if (!cm) + return -ENOMEM; + + /* Basic Values. Unspecified are Null or 0 */ + cm->dev = &pdev->dev; + cm->desc = desc; + psy_cfg.drv_data = cm; + + /* Initialize alarm timer */ + if (alarmtimer_get_rtcdev()) { + cm_timer = devm_kzalloc(cm->dev, sizeof(*cm_timer), GFP_KERNEL); + if (!cm_timer) + return -ENOMEM; + alarm_init(cm_timer, ALARM_BOOTTIME, cm_timer_func); + } + + /* + * Some of the following do not need to be errors. + * Users may intentionally ignore those features. + */ + if (desc->fullbatt_uV == 0) { + dev_info(&pdev->dev, "Ignoring full-battery voltage threshold as it is not supplied\n"); + } + if (!desc->fullbatt_vchkdrop_uV) { + dev_info(&pdev->dev, "Disabling full-battery voltage drop checking mechanism as it is not supplied\n"); + desc->fullbatt_vchkdrop_uV = 0; + } + if (desc->fullbatt_soc == 0) { + dev_info(&pdev->dev, "Ignoring full-battery soc(state of charge) threshold as it is not supplied\n"); + } + if (desc->fullbatt_full_capacity == 0) { + dev_info(&pdev->dev, "Ignoring full-battery full capacity threshold as it is not supplied\n"); + } + + if (!desc->charger_regulators || desc->num_charger_regulators < 1) { + dev_err(&pdev->dev, "charger_regulators undefined\n"); + return -EINVAL; + } + + if (!desc->psy_charger_stat || !desc->psy_charger_stat[0]) { + dev_err(&pdev->dev, "No power supply defined\n"); + return -EINVAL; + } + + if (!desc->psy_fuel_gauge) { + dev_err(&pdev->dev, "No fuel gauge power supply defined\n"); + return -EINVAL; + } + + /* Check if charger's supplies are present at probe */ + for (i = 0; desc->psy_charger_stat[i]; i++) { + struct power_supply *psy; + + psy = power_supply_get_by_name(desc->psy_charger_stat[i]); + if (!psy) { + dev_err(&pdev->dev, "Cannot find power supply \"%s\"\n", + desc->psy_charger_stat[i]); + return -ENODEV; + } + power_supply_put(psy); + } + + if (cm->desc->polling_mode != CM_POLL_DISABLE && + (desc->polling_interval_ms == 0 || + msecs_to_jiffies(desc->polling_interval_ms) <= CM_JIFFIES_SMALL)) { + dev_err(&pdev->dev, "polling_interval_ms is too small\n"); + return -EINVAL; + } + + if (!desc->charging_max_duration_ms || + !desc->discharging_max_duration_ms) { + dev_info(&pdev->dev, "Cannot limit charging duration checking mechanism to prevent overcharge/overheat and control discharging duration\n"); + desc->charging_max_duration_ms = 0; + desc->discharging_max_duration_ms = 0; + } + + platform_set_drvdata(pdev, cm); + + memcpy(&cm->charger_psy_desc, &psy_default, sizeof(psy_default)); + + if (!desc->psy_name) + strncpy(cm->psy_name_buf, psy_default.name, PSY_NAME_MAX); + else + strncpy(cm->psy_name_buf, desc->psy_name, PSY_NAME_MAX); + cm->charger_psy_desc.name = cm->psy_name_buf; + + /* Allocate for psy properties because they may vary */ + properties = devm_kcalloc(&pdev->dev, + ARRAY_SIZE(default_charger_props) + + NUM_CHARGER_PSY_OPTIONAL, + sizeof(*properties), GFP_KERNEL); + if (!properties) + return -ENOMEM; + + memcpy(properties, default_charger_props, + sizeof(enum power_supply_property) * + ARRAY_SIZE(default_charger_props)); + num_properties = ARRAY_SIZE(default_charger_props); + + /* Find which optional psy-properties are available */ + fuel_gauge = power_supply_get_by_name(desc->psy_fuel_gauge); + if (!fuel_gauge) { + dev_err(&pdev->dev, "Cannot find power supply \"%s\"\n", + desc->psy_fuel_gauge); + return -ENODEV; + } + if (!power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CHARGE_FULL, &val)) { + properties[num_properties] = + POWER_SUPPLY_PROP_CHARGE_FULL; + num_properties++; + } + if (!power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CHARGE_NOW, &val)) { + properties[num_properties] = + POWER_SUPPLY_PROP_CHARGE_NOW; + num_properties++; + } + if (!power_supply_get_property(fuel_gauge, + POWER_SUPPLY_PROP_CURRENT_NOW, + &val)) { + properties[num_properties] = + POWER_SUPPLY_PROP_CURRENT_NOW; + num_properties++; + } + + ret = cm_init_thermal_data(cm, fuel_gauge, properties, &num_properties); + if (ret) { + dev_err(&pdev->dev, "Failed to initialize thermal data\n"); + cm->desc->measure_battery_temp = false; + } + power_supply_put(fuel_gauge); + + cm->charger_psy_desc.properties = properties; + cm->charger_psy_desc.num_properties = num_properties; + + /* Register sysfs entry for charger(regulator) */ + ret = charger_manager_prepare_sysfs(cm); + if (ret < 0) { + dev_err(&pdev->dev, + "Cannot prepare sysfs entry of regulators\n"); + return ret; + } + psy_cfg.attr_grp = desc->sysfs_groups; + + cm->charger_psy = power_supply_register(&pdev->dev, + &cm->charger_psy_desc, + &psy_cfg); + if (IS_ERR(cm->charger_psy)) { + dev_err(&pdev->dev, "Cannot register charger-manager with name \"%s\"\n", + cm->charger_psy_desc.name); + return PTR_ERR(cm->charger_psy); + } + + /* Register extcon device for charger cable */ + ret = charger_manager_register_extcon(cm); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot initialize extcon device\n"); + goto err_reg_extcon; + } + + /* Add to the list */ + mutex_lock(&cm_list_mtx); + list_add(&cm->entry, &cm_list); + mutex_unlock(&cm_list_mtx); + + /* + * Charger-manager is capable of waking up the systme from sleep + * when event is happened through cm_notify_event() + */ + device_init_wakeup(&pdev->dev, true); + device_set_wakeup_capable(&pdev->dev, false); + + /* + * Charger-manager have to check the charging state right after + * initialization of charger-manager and then update current charging + * state. + */ + cm_monitor(); + + schedule_work(&setup_polling); + + return 0; + +err_reg_extcon: + for (i = 0; i < desc->num_charger_regulators; i++) + regulator_put(desc->charger_regulators[i].consumer); + + power_supply_unregister(cm->charger_psy); + + return ret; +} + +static int charger_manager_remove(struct platform_device *pdev) +{ + struct charger_manager *cm = platform_get_drvdata(pdev); + struct charger_desc *desc = cm->desc; + int i = 0; + + /* Remove from the list */ + mutex_lock(&cm_list_mtx); + list_del(&cm->entry); + mutex_unlock(&cm_list_mtx); + + cancel_work_sync(&setup_polling); + cancel_delayed_work_sync(&cm_monitor_work); + + for (i = 0 ; i < desc->num_charger_regulators ; i++) + regulator_put(desc->charger_regulators[i].consumer); + + power_supply_unregister(cm->charger_psy); + + try_charger_enable(cm, false); + + return 0; +} + +static const struct platform_device_id charger_manager_id[] = { + { "charger-manager", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(platform, charger_manager_id); + +static int cm_suspend_noirq(struct device *dev) +{ + if (device_may_wakeup(dev)) { + device_set_wakeup_capable(dev, false); + return -EAGAIN; + } + + return 0; +} + +static bool cm_need_to_awake(void) +{ + struct charger_manager *cm; + + if (cm_timer) + return false; + + mutex_lock(&cm_list_mtx); + list_for_each_entry(cm, &cm_list, entry) { + if (is_charging(cm)) { + mutex_unlock(&cm_list_mtx); + return true; + } + } + mutex_unlock(&cm_list_mtx); + + return false; +} + +static int cm_suspend_prepare(struct device *dev) +{ + if (cm_need_to_awake()) + return -EBUSY; + + if (!cm_suspended) + cm_suspended = true; + + cm_timer_set = cm_setup_timer(); + + if (cm_timer_set) { + cancel_work_sync(&setup_polling); + cancel_delayed_work_sync(&cm_monitor_work); + } + + return 0; +} + +static void cm_suspend_complete(struct device *dev) +{ + struct charger_manager *cm = dev_get_drvdata(dev); + + if (cm_suspended) + cm_suspended = false; + + if (cm_timer_set) { + ktime_t remain; + + alarm_cancel(cm_timer); + cm_timer_set = false; + remain = alarm_expires_remaining(cm_timer); + cm_suspend_duration_ms -= ktime_to_ms(remain); + schedule_work(&setup_polling); + } + + _cm_monitor(cm); + + device_set_wakeup_capable(cm->dev, false); +} + +static const struct dev_pm_ops charger_manager_pm = { + .prepare = cm_suspend_prepare, + .suspend_noirq = cm_suspend_noirq, + .complete = cm_suspend_complete, +}; + +static struct platform_driver charger_manager_driver = { + .driver = { + .name = "charger-manager", + .pm = &charger_manager_pm, + .of_match_table = charger_manager_match, + }, + .probe = charger_manager_probe, + .remove = charger_manager_remove, + .id_table = charger_manager_id, +}; + +static int __init charger_manager_init(void) +{ + cm_wq = create_freezable_workqueue("charger_manager"); + if (unlikely(!cm_wq)) + return -ENOMEM; + + INIT_DELAYED_WORK(&cm_monitor_work, cm_monitor_poller); + + return platform_driver_register(&charger_manager_driver); +} +late_initcall(charger_manager_init); + +static void __exit charger_manager_cleanup(void) +{ + destroy_workqueue(cm_wq); + cm_wq = NULL; + + platform_driver_unregister(&charger_manager_driver); +} +module_exit(charger_manager_cleanup); + +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("Charger Manager"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/collie_battery.c b/drivers/power/supply/collie_battery.c new file mode 100644 index 000000000..cbd588e9e --- /dev/null +++ b/drivers/power/supply/collie_battery.c @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery and Power Management code for the Sharp SL-5x00 + * + * Copyright (C) 2009 Thomas Kunze + * + * based on tosa_battery.c + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/power_supply.h> +#include <linux/delay.h> +#include <linux/spinlock.h> +#include <linux/interrupt.h> +#include <linux/gpio.h> +#include <linux/mfd/ucb1x00.h> + +#include <asm/mach/sharpsl_param.h> +#include <asm/mach-types.h> +#include <mach/collie.h> + +static DEFINE_MUTEX(bat_lock); /* protects gpio pins */ +static struct work_struct bat_work; +static struct ucb1x00 *ucb; + +struct collie_bat { + int status; + struct power_supply *psy; + int full_chrg; + + struct mutex work_lock; /* protects data */ + + bool (*is_present)(struct collie_bat *bat); + int gpio_full; + int gpio_charge_on; + + int technology; + + int gpio_bat; + int adc_bat; + int adc_bat_divider; + int bat_max; + int bat_min; + + int gpio_temp; + int adc_temp; + int adc_temp_divider; +}; + +static struct collie_bat collie_bat_main; + +static unsigned long collie_read_bat(struct collie_bat *bat) +{ + unsigned long value = 0; + + if (bat->gpio_bat < 0 || bat->adc_bat < 0) + return 0; + mutex_lock(&bat_lock); + gpio_set_value(bat->gpio_bat, 1); + msleep(5); + ucb1x00_adc_enable(ucb); + value = ucb1x00_adc_read(ucb, bat->adc_bat, UCB_SYNC); + ucb1x00_adc_disable(ucb); + gpio_set_value(bat->gpio_bat, 0); + mutex_unlock(&bat_lock); + value = value * 1000000 / bat->adc_bat_divider; + + return value; +} + +static unsigned long collie_read_temp(struct collie_bat *bat) +{ + unsigned long value = 0; + if (bat->gpio_temp < 0 || bat->adc_temp < 0) + return 0; + + mutex_lock(&bat_lock); + gpio_set_value(bat->gpio_temp, 1); + msleep(5); + ucb1x00_adc_enable(ucb); + value = ucb1x00_adc_read(ucb, bat->adc_temp, UCB_SYNC); + ucb1x00_adc_disable(ucb); + gpio_set_value(bat->gpio_temp, 0); + mutex_unlock(&bat_lock); + + value = value * 10000 / bat->adc_temp_divider; + + return value; +} + +static int collie_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct collie_bat *bat = power_supply_get_drvdata(psy); + + if (bat->is_present && !bat->is_present(bat) + && psp != POWER_SUPPLY_PROP_PRESENT) { + return -ENODEV; + } + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = bat->status; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = bat->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = collie_read_bat(bat); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + if (bat->full_chrg == -1) + val->intval = bat->bat_max; + else + val->intval = bat->full_chrg; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = bat->bat_max; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = bat->bat_min; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = collie_read_temp(bat); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = bat->is_present ? bat->is_present(bat) : 1; + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static void collie_bat_external_power_changed(struct power_supply *psy) +{ + schedule_work(&bat_work); +} + +static irqreturn_t collie_bat_gpio_isr(int irq, void *data) +{ + pr_info("collie_bat_gpio irq\n"); + schedule_work(&bat_work); + return IRQ_HANDLED; +} + +static void collie_bat_update(struct collie_bat *bat) +{ + int old; + struct power_supply *psy = bat->psy; + + mutex_lock(&bat->work_lock); + + old = bat->status; + + if (bat->is_present && !bat->is_present(bat)) { + printk(KERN_NOTICE "%s not present\n", psy->desc->name); + bat->status = POWER_SUPPLY_STATUS_UNKNOWN; + bat->full_chrg = -1; + } else if (power_supply_am_i_supplied(psy)) { + if (bat->status == POWER_SUPPLY_STATUS_DISCHARGING) { + gpio_set_value(bat->gpio_charge_on, 1); + mdelay(15); + } + + if (gpio_get_value(bat->gpio_full)) { + if (old == POWER_SUPPLY_STATUS_CHARGING || + bat->full_chrg == -1) + bat->full_chrg = collie_read_bat(bat); + + gpio_set_value(bat->gpio_charge_on, 0); + bat->status = POWER_SUPPLY_STATUS_FULL; + } else { + gpio_set_value(bat->gpio_charge_on, 1); + bat->status = POWER_SUPPLY_STATUS_CHARGING; + } + } else { + gpio_set_value(bat->gpio_charge_on, 0); + bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + } + + if (old != bat->status) + power_supply_changed(psy); + + mutex_unlock(&bat->work_lock); +} + +static void collie_bat_work(struct work_struct *work) +{ + collie_bat_update(&collie_bat_main); +} + + +static enum power_supply_property collie_bat_main_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TEMP, +}; + +static enum power_supply_property collie_bat_bu_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_PRESENT, +}; + +static const struct power_supply_desc collie_bat_main_desc = { + .name = "main-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = collie_bat_main_props, + .num_properties = ARRAY_SIZE(collie_bat_main_props), + .get_property = collie_bat_get_property, + .external_power_changed = collie_bat_external_power_changed, + .use_for_apm = 1, +}; + +static struct collie_bat collie_bat_main = { + .status = POWER_SUPPLY_STATUS_DISCHARGING, + .full_chrg = -1, + .psy = NULL, + + .gpio_full = COLLIE_GPIO_CO, + .gpio_charge_on = COLLIE_GPIO_CHARGE_ON, + + .technology = POWER_SUPPLY_TECHNOLOGY_LIPO, + + .gpio_bat = COLLIE_GPIO_MBAT_ON, + .adc_bat = UCB_ADC_INP_AD1, + .adc_bat_divider = 155, + .bat_max = 4310000, + .bat_min = 1551 * 1000000 / 414, + + .gpio_temp = COLLIE_GPIO_TMP_ON, + .adc_temp = UCB_ADC_INP_AD0, + .adc_temp_divider = 10000, +}; + +static const struct power_supply_desc collie_bat_bu_desc = { + .name = "backup-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = collie_bat_bu_props, + .num_properties = ARRAY_SIZE(collie_bat_bu_props), + .get_property = collie_bat_get_property, + .external_power_changed = collie_bat_external_power_changed, +}; + +static struct collie_bat collie_bat_bu = { + .status = POWER_SUPPLY_STATUS_UNKNOWN, + .full_chrg = -1, + .psy = NULL, + + .gpio_full = -1, + .gpio_charge_on = -1, + + .technology = POWER_SUPPLY_TECHNOLOGY_LiMn, + + .gpio_bat = COLLIE_GPIO_BBAT_ON, + .adc_bat = UCB_ADC_INP_AD1, + .adc_bat_divider = 155, + .bat_max = 3000000, + .bat_min = 1900000, + + .gpio_temp = -1, + .adc_temp = -1, + .adc_temp_divider = -1, +}; + +static struct gpio collie_batt_gpios[] = { + { COLLIE_GPIO_CO, GPIOF_IN, "main battery full" }, + { COLLIE_GPIO_MAIN_BAT_LOW, GPIOF_IN, "main battery low" }, + { COLLIE_GPIO_CHARGE_ON, GPIOF_OUT_INIT_LOW, "main charge on" }, + { COLLIE_GPIO_MBAT_ON, GPIOF_OUT_INIT_LOW, "main battery" }, + { COLLIE_GPIO_TMP_ON, GPIOF_OUT_INIT_LOW, "main battery temp" }, + { COLLIE_GPIO_BBAT_ON, GPIOF_OUT_INIT_LOW, "backup battery" }, +}; + +#ifdef CONFIG_PM +static int wakeup_enabled; + +static int collie_bat_suspend(struct ucb1x00_dev *dev) +{ + /* flush all pending status updates */ + flush_work(&bat_work); + + if (device_may_wakeup(&dev->ucb->dev) && + collie_bat_main.status == POWER_SUPPLY_STATUS_CHARGING) + wakeup_enabled = !enable_irq_wake(gpio_to_irq(COLLIE_GPIO_CO)); + else + wakeup_enabled = 0; + + return 0; +} + +static int collie_bat_resume(struct ucb1x00_dev *dev) +{ + if (wakeup_enabled) + disable_irq_wake(gpio_to_irq(COLLIE_GPIO_CO)); + + /* things may have changed while we were away */ + schedule_work(&bat_work); + return 0; +} +#else +#define collie_bat_suspend NULL +#define collie_bat_resume NULL +#endif + +static int collie_bat_probe(struct ucb1x00_dev *dev) +{ + int ret; + struct power_supply_config psy_main_cfg = {}, psy_bu_cfg = {}; + + if (!machine_is_collie()) + return -ENODEV; + + ucb = dev->ucb; + + ret = gpio_request_array(collie_batt_gpios, + ARRAY_SIZE(collie_batt_gpios)); + if (ret) + return ret; + + mutex_init(&collie_bat_main.work_lock); + + INIT_WORK(&bat_work, collie_bat_work); + + psy_main_cfg.drv_data = &collie_bat_main; + collie_bat_main.psy = power_supply_register(&dev->ucb->dev, + &collie_bat_main_desc, + &psy_main_cfg); + if (IS_ERR(collie_bat_main.psy)) { + ret = PTR_ERR(collie_bat_main.psy); + goto err_psy_reg_main; + } + + psy_bu_cfg.drv_data = &collie_bat_bu; + collie_bat_bu.psy = power_supply_register(&dev->ucb->dev, + &collie_bat_bu_desc, + &psy_bu_cfg); + if (IS_ERR(collie_bat_bu.psy)) { + ret = PTR_ERR(collie_bat_bu.psy); + goto err_psy_reg_bu; + } + + ret = request_irq(gpio_to_irq(COLLIE_GPIO_CO), + collie_bat_gpio_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "main full", &collie_bat_main); + if (ret) + goto err_irq; + + device_init_wakeup(&ucb->dev, 1); + schedule_work(&bat_work); + + return 0; + +err_irq: + power_supply_unregister(collie_bat_bu.psy); +err_psy_reg_bu: + power_supply_unregister(collie_bat_main.psy); +err_psy_reg_main: + + /* see comment in collie_bat_remove */ + cancel_work_sync(&bat_work); + gpio_free_array(collie_batt_gpios, ARRAY_SIZE(collie_batt_gpios)); + return ret; +} + +static void collie_bat_remove(struct ucb1x00_dev *dev) +{ + free_irq(gpio_to_irq(COLLIE_GPIO_CO), &collie_bat_main); + + power_supply_unregister(collie_bat_bu.psy); + power_supply_unregister(collie_bat_main.psy); + + /* + * Now cancel the bat_work. We won't get any more schedules, + * since all sources (isr and external_power_changed) are + * unregistered now. + */ + cancel_work_sync(&bat_work); + gpio_free_array(collie_batt_gpios, ARRAY_SIZE(collie_batt_gpios)); +} + +static struct ucb1x00_driver collie_bat_driver = { + .add = collie_bat_probe, + .remove = collie_bat_remove, + .suspend = collie_bat_suspend, + .resume = collie_bat_resume, +}; + +static int __init collie_bat_init(void) +{ + return ucb1x00_register_driver(&collie_bat_driver); +} + +static void __exit collie_bat_exit(void) +{ + ucb1x00_unregister_driver(&collie_bat_driver); +} + +module_init(collie_bat_init); +module_exit(collie_bat_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Thomas Kunze"); +MODULE_DESCRIPTION("Collie battery driver"); diff --git a/drivers/power/supply/cpcap-battery.c b/drivers/power/supply/cpcap-battery.c new file mode 100644 index 000000000..793d4ca52 --- /dev/null +++ b/drivers/power/supply/cpcap-battery.c @@ -0,0 +1,957 @@ +/* + * Battery driver for CPCAP PMIC + * + * Copyright (C) 2017 Tony Lindgren <tony@atomide.com> + * + * Some parts of the code based on earlie Motorola mapphone Linux kernel + * drivers: + * + * Copyright (C) 2009-2010 Motorola, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +#include <linux/iio/consumer.h> +#include <linux/iio/types.h> +#include <linux/mfd/motorola-cpcap.h> + +/* + * Register bit defines for CPCAP_REG_BPEOL. Some of these seem to + * map to MC13783UG.pdf "Table 5-19. Register 13, Power Control 0" + * to enable BATTDETEN, LOBAT and EOL features. We currently use + * LOBAT interrupts instead of EOL. + */ +#define CPCAP_REG_BPEOL_BIT_EOL9 BIT(9) /* Set for EOL irq */ +#define CPCAP_REG_BPEOL_BIT_EOL8 BIT(8) /* Set for EOL irq */ +#define CPCAP_REG_BPEOL_BIT_UNKNOWN7 BIT(7) +#define CPCAP_REG_BPEOL_BIT_UNKNOWN6 BIT(6) +#define CPCAP_REG_BPEOL_BIT_UNKNOWN5 BIT(5) +#define CPCAP_REG_BPEOL_BIT_EOL_MULTI BIT(4) /* Set for multiple EOL irqs */ +#define CPCAP_REG_BPEOL_BIT_UNKNOWN3 BIT(3) +#define CPCAP_REG_BPEOL_BIT_UNKNOWN2 BIT(2) +#define CPCAP_REG_BPEOL_BIT_BATTDETEN BIT(1) /* Enable battery detect */ +#define CPCAP_REG_BPEOL_BIT_EOLSEL BIT(0) /* BPDET = 0, EOL = 1 */ + +/* + * Register bit defines for CPCAP_REG_CCC1. These seem similar to the twl6030 + * coulomb counter registers rather than the mc13892 registers. Both twl6030 + * and mc13892 set bits 2 and 1 to reset and clear registers. But mc13892 + * sets bit 0 to start the coulomb counter while twl6030 sets bit 0 to stop + * the coulomb counter like cpcap does. So for now, we use the twl6030 style + * naming for the registers. + */ +#define CPCAP_REG_CCC1_ACTIVE_MODE1 BIT(4) /* Update rate */ +#define CPCAP_REG_CCC1_ACTIVE_MODE0 BIT(3) /* Update rate */ +#define CPCAP_REG_CCC1_AUTOCLEAR BIT(2) /* Resets sample registers */ +#define CPCAP_REG_CCC1_CAL_EN BIT(1) /* Clears after write in 1s */ +#define CPCAP_REG_CCC1_PAUSE BIT(0) /* Stop counters, allow write */ +#define CPCAP_REG_CCC1_RESET_MASK (CPCAP_REG_CCC1_AUTOCLEAR | \ + CPCAP_REG_CCC1_CAL_EN) + +#define CPCAP_REG_CCCC2_RATE1 BIT(5) +#define CPCAP_REG_CCCC2_RATE0 BIT(4) +#define CPCAP_REG_CCCC2_ENABLE BIT(3) + +#define CPCAP_BATTERY_CC_SAMPLE_PERIOD_MS 250 + +enum { + CPCAP_BATTERY_IIO_BATTDET, + CPCAP_BATTERY_IIO_VOLTAGE, + CPCAP_BATTERY_IIO_CHRG_CURRENT, + CPCAP_BATTERY_IIO_BATT_CURRENT, + CPCAP_BATTERY_IIO_NR, +}; + +enum cpcap_battery_irq_action { + CPCAP_BATTERY_IRQ_ACTION_NONE, + CPCAP_BATTERY_IRQ_ACTION_CC_CAL_DONE, + CPCAP_BATTERY_IRQ_ACTION_BATTERY_LOW, + CPCAP_BATTERY_IRQ_ACTION_POWEROFF, +}; + +struct cpcap_interrupt_desc { + const char *name; + struct list_head node; + int irq; + enum cpcap_battery_irq_action action; +}; + +struct cpcap_battery_config { + int cd_factor; + struct power_supply_info info; + struct power_supply_battery_info bat; +}; + +struct cpcap_coulomb_counter_data { + s32 sample; /* 24 or 32 bits */ + s32 accumulator; + s16 offset; /* 9 bits */ + s16 integrator; /* 13 or 16 bits */ +}; + +enum cpcap_battery_state { + CPCAP_BATTERY_STATE_PREVIOUS, + CPCAP_BATTERY_STATE_LATEST, + CPCAP_BATTERY_STATE_NR, +}; + +struct cpcap_battery_state_data { + int voltage; + int current_ua; + int counter_uah; + int temperature; + ktime_t time; + struct cpcap_coulomb_counter_data cc; +}; + +struct cpcap_battery_ddata { + struct device *dev; + struct regmap *reg; + struct list_head irq_list; + struct iio_channel *channels[CPCAP_BATTERY_IIO_NR]; + struct power_supply *psy; + struct cpcap_battery_config config; + struct cpcap_battery_state_data state[CPCAP_BATTERY_STATE_NR]; + u32 cc_lsb; /* μAms per LSB */ + atomic_t active; + int status; + u16 vendor; +}; + +#define CPCAP_NO_BATTERY -400 + +static struct cpcap_battery_state_data * +cpcap_battery_get_state(struct cpcap_battery_ddata *ddata, + enum cpcap_battery_state state) +{ + if (state >= CPCAP_BATTERY_STATE_NR) + return NULL; + + return &ddata->state[state]; +} + +static struct cpcap_battery_state_data * +cpcap_battery_latest(struct cpcap_battery_ddata *ddata) +{ + return cpcap_battery_get_state(ddata, CPCAP_BATTERY_STATE_LATEST); +} + +static struct cpcap_battery_state_data * +cpcap_battery_previous(struct cpcap_battery_ddata *ddata) +{ + return cpcap_battery_get_state(ddata, CPCAP_BATTERY_STATE_PREVIOUS); +} + +static int cpcap_charger_battery_temperature(struct cpcap_battery_ddata *ddata, + int *value) +{ + struct iio_channel *channel; + int error; + + channel = ddata->channels[CPCAP_BATTERY_IIO_BATTDET]; + error = iio_read_channel_processed(channel, value); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + *value = CPCAP_NO_BATTERY; + + return error; + } + + *value /= 100; + + return 0; +} + +static int cpcap_battery_get_voltage(struct cpcap_battery_ddata *ddata) +{ + struct iio_channel *channel; + int error, value = 0; + + channel = ddata->channels[CPCAP_BATTERY_IIO_VOLTAGE]; + error = iio_read_channel_processed(channel, &value); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + + return 0; + } + + return value * 1000; +} + +static int cpcap_battery_get_current(struct cpcap_battery_ddata *ddata) +{ + struct iio_channel *channel; + int error, value = 0; + + channel = ddata->channels[CPCAP_BATTERY_IIO_BATT_CURRENT]; + error = iio_read_channel_processed(channel, &value); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + + return 0; + } + + return value * 1000; +} + +/** + * cpcap_battery_cc_raw_div - calculate and divide coulomb counter μAms values + * @ddata: device driver data + * @sample: coulomb counter sample value + * @accumulator: coulomb counter integrator value + * @offset: coulomb counter offset value + * @divider: conversion divider + * + * Note that cc_lsb and cc_dur values are from Motorola Linux kernel + * function data_get_avg_curr_ua() and seem to be based on measured test + * results. It also has the following comment: + * + * Adjustment factors are applied here as a temp solution per the test + * results. Need to work out a formal solution for this adjustment. + * + * A coulomb counter for similar hardware seems to be documented in + * "TWL6030 Gas Gauging Basics (Rev. A)" swca095a.pdf in chapter + * "10 Calculating Accumulated Current". We however follow what the + * Motorola mapphone Linux kernel is doing as there may be either a + * TI or ST coulomb counter in the PMIC. + */ +static int cpcap_battery_cc_raw_div(struct cpcap_battery_ddata *ddata, + s32 sample, s32 accumulator, + s16 offset, u32 divider) +{ + s64 acc; + + if (!divider) + return 0; + + acc = accumulator; + acc -= (s64)sample * offset; + acc *= ddata->cc_lsb; + acc *= -1; + acc = div_s64(acc, divider); + + return acc; +} + +/* 3600000μAms = 1μAh */ +static int cpcap_battery_cc_to_uah(struct cpcap_battery_ddata *ddata, + s32 sample, s32 accumulator, + s16 offset) +{ + return cpcap_battery_cc_raw_div(ddata, sample, + accumulator, offset, + 3600000); +} + +static int cpcap_battery_cc_to_ua(struct cpcap_battery_ddata *ddata, + s32 sample, s32 accumulator, + s16 offset) +{ + return cpcap_battery_cc_raw_div(ddata, sample, + accumulator, offset, + sample * + CPCAP_BATTERY_CC_SAMPLE_PERIOD_MS); +} + +/** + * cpcap_battery_read_accumulated - reads cpcap coulomb counter + * @ddata: device driver data + * @ccd: coulomb counter values + * + * Based on Motorola mapphone kernel function data_read_regs(). + * Looking at the registers, the coulomb counter seems similar to + * the coulomb counter in TWL6030. See "TWL6030 Gas Gauging Basics + * (Rev. A) swca095a.pdf for "10 Calculating Accumulated Current". + * + * Note that swca095a.pdf instructs to stop the coulomb counter + * before reading to avoid values changing. Motorola mapphone + * Linux kernel does not do it, so let's assume they've verified + * the data produced is correct. + */ +static int +cpcap_battery_read_accumulated(struct cpcap_battery_ddata *ddata, + struct cpcap_coulomb_counter_data *ccd) +{ + u16 buf[7]; /* CPCAP_REG_CCS1 to CCI */ + int error; + + ccd->sample = 0; + ccd->accumulator = 0; + ccd->offset = 0; + ccd->integrator = 0; + + /* Read coulomb counter register range */ + error = regmap_bulk_read(ddata->reg, CPCAP_REG_CCS1, + buf, ARRAY_SIZE(buf)); + if (error) + return 0; + + /* Sample value CPCAP_REG_CCS1 & 2 */ + ccd->sample = (buf[1] & 0x0fff) << 16; + ccd->sample |= buf[0]; + if (ddata->vendor == CPCAP_VENDOR_TI) + ccd->sample = sign_extend32(24, ccd->sample); + + /* Accumulator value CPCAP_REG_CCA1 & 2 */ + ccd->accumulator = ((s16)buf[3]) << 16; + ccd->accumulator |= buf[2]; + + /* + * Coulomb counter calibration offset is CPCAP_REG_CCM, + * REG_CCO seems unused + */ + ccd->offset = buf[4]; + ccd->offset = sign_extend32(ccd->offset, 9); + + /* Integrator register CPCAP_REG_CCI */ + if (ddata->vendor == CPCAP_VENDOR_TI) + ccd->integrator = sign_extend32(buf[6], 13); + else + ccd->integrator = (s16)buf[6]; + + return cpcap_battery_cc_to_uah(ddata, + ccd->sample, + ccd->accumulator, + ccd->offset); +} + +/** + * cpcap_battery_cc_get_avg_current - read cpcap coulumb counter + * @ddata: cpcap battery driver device data + */ +static int cpcap_battery_cc_get_avg_current(struct cpcap_battery_ddata *ddata) +{ + int value, acc, error; + s32 sample; + s16 offset; + + /* Coulomb counter integrator */ + error = regmap_read(ddata->reg, CPCAP_REG_CCI, &value); + if (error) + return error; + + if (ddata->vendor == CPCAP_VENDOR_TI) { + acc = sign_extend32(value, 13); + sample = 1; + } else { + acc = (s16)value; + sample = 4; + } + + /* Coulomb counter calibration offset */ + error = regmap_read(ddata->reg, CPCAP_REG_CCM, &value); + if (error) + return error; + + offset = sign_extend32(value, 9); + + return cpcap_battery_cc_to_ua(ddata, sample, acc, offset); +} + +static bool cpcap_battery_full(struct cpcap_battery_ddata *ddata) +{ + struct cpcap_battery_state_data *state = cpcap_battery_latest(ddata); + + if (state->voltage >= + (ddata->config.bat.constant_charge_voltage_max_uv - 18000)) + return true; + + return false; +} + +static int cpcap_battery_update_status(struct cpcap_battery_ddata *ddata) +{ + struct cpcap_battery_state_data state, *latest, *previous; + ktime_t now; + int error; + + memset(&state, 0, sizeof(state)); + now = ktime_get(); + + latest = cpcap_battery_latest(ddata); + if (latest) { + s64 delta_ms = ktime_to_ms(ktime_sub(now, latest->time)); + + if (delta_ms < CPCAP_BATTERY_CC_SAMPLE_PERIOD_MS) + return delta_ms; + } + + state.time = now; + state.voltage = cpcap_battery_get_voltage(ddata); + state.current_ua = cpcap_battery_get_current(ddata); + state.counter_uah = cpcap_battery_read_accumulated(ddata, &state.cc); + + error = cpcap_charger_battery_temperature(ddata, + &state.temperature); + if (error) + return error; + + previous = cpcap_battery_previous(ddata); + memcpy(previous, latest, sizeof(*previous)); + memcpy(latest, &state, sizeof(*latest)); + + return 0; +} + +static enum power_supply_property cpcap_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_POWER_NOW, + POWER_SUPPLY_PROP_POWER_AVG, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_TEMP, +}; + +static int cpcap_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct cpcap_battery_ddata *ddata = power_supply_get_drvdata(psy); + struct cpcap_battery_state_data *latest, *previous; + u32 sample; + s32 accumulator; + int cached; + s64 tmp; + + cached = cpcap_battery_update_status(ddata); + if (cached < 0) + return cached; + + latest = cpcap_battery_latest(ddata); + previous = cpcap_battery_previous(ddata); + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + if (latest->temperature > CPCAP_NO_BATTERY) + val->intval = 1; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_STATUS: + if (cpcap_battery_full(ddata)) { + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + } + if (cpcap_battery_cc_get_avg_current(ddata) < 0) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = ddata->config.info.technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = cpcap_battery_get_voltage(ddata); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = ddata->config.info.voltage_max_design; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = ddata->config.info.voltage_min_design; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + val->intval = ddata->config.bat.constant_charge_voltage_max_uv; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + sample = latest->cc.sample - previous->cc.sample; + if (!sample) { + val->intval = cpcap_battery_cc_get_avg_current(ddata); + break; + } + accumulator = latest->cc.accumulator - previous->cc.accumulator; + val->intval = cpcap_battery_cc_to_ua(ddata, sample, + accumulator, + latest->cc.offset); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = latest->current_ua; + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + val->intval = latest->counter_uah; + break; + case POWER_SUPPLY_PROP_POWER_NOW: + tmp = (latest->voltage / 10000) * latest->current_ua; + val->intval = div64_s64(tmp, 100); + break; + case POWER_SUPPLY_PROP_POWER_AVG: + sample = latest->cc.sample - previous->cc.sample; + if (!sample) { + tmp = cpcap_battery_cc_get_avg_current(ddata); + tmp *= (latest->voltage / 10000); + val->intval = div64_s64(tmp, 100); + break; + } + accumulator = latest->cc.accumulator - previous->cc.accumulator; + tmp = cpcap_battery_cc_to_ua(ddata, sample, accumulator, + latest->cc.offset); + tmp *= ((latest->voltage + previous->voltage) / 20000); + val->intval = div64_s64(tmp, 100); + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + if (cpcap_battery_full(ddata)) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (latest->voltage >= 3750000) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else if (latest->voltage >= 3300000) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + else if (latest->voltage > 3100000) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (latest->voltage <= 3100000) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = ddata->config.info.charge_full_design; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = latest->temperature; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int cpcap_battery_update_charger(struct cpcap_battery_ddata *ddata, + int const_charge_voltage) +{ + union power_supply_propval prop; + union power_supply_propval val; + struct power_supply *charger; + int error; + + charger = power_supply_get_by_name("usb"); + if (!charger) + return -ENODEV; + + error = power_supply_get_property(charger, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + &prop); + if (error) + goto out_put; + + /* Allow charger const voltage lower than battery const voltage */ + if (const_charge_voltage > prop.intval) + goto out_put; + + val.intval = const_charge_voltage; + + error = power_supply_set_property(charger, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + &val); +out_put: + power_supply_put(charger); + + return error; +} + +static int cpcap_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct cpcap_battery_ddata *ddata = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + if (val->intval < ddata->config.info.voltage_min_design) + return -EINVAL; + if (val->intval > ddata->config.info.voltage_max_design) + return -EINVAL; + + ddata->config.bat.constant_charge_voltage_max_uv = val->intval; + + return cpcap_battery_update_charger(ddata, val->intval); + default: + return -EINVAL; + } + + return 0; +} + +static int cpcap_battery_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + return 1; + default: + return 0; + } +} + +static irqreturn_t cpcap_battery_irq_thread(int irq, void *data) +{ + struct cpcap_battery_ddata *ddata = data; + struct cpcap_battery_state_data *latest; + struct cpcap_interrupt_desc *d; + + if (!atomic_read(&ddata->active)) + return IRQ_NONE; + + list_for_each_entry(d, &ddata->irq_list, node) { + if (irq == d->irq) + break; + } + + if (list_entry_is_head(d, &ddata->irq_list, node)) + return IRQ_NONE; + + latest = cpcap_battery_latest(ddata); + + switch (d->action) { + case CPCAP_BATTERY_IRQ_ACTION_CC_CAL_DONE: + dev_info(ddata->dev, "Coulomb counter calibration done\n"); + break; + case CPCAP_BATTERY_IRQ_ACTION_BATTERY_LOW: + if (latest->current_ua >= 0) + dev_warn(ddata->dev, "Battery low at %imV!\n", + latest->voltage / 1000); + break; + case CPCAP_BATTERY_IRQ_ACTION_POWEROFF: + if (latest->current_ua >= 0 && latest->voltage <= 3200000) { + dev_emerg(ddata->dev, + "Battery empty at %imV, powering off\n", + latest->voltage / 1000); + orderly_poweroff(true); + } + break; + default: + break; + } + + power_supply_changed(ddata->psy); + + return IRQ_HANDLED; +} + +static int cpcap_battery_init_irq(struct platform_device *pdev, + struct cpcap_battery_ddata *ddata, + const char *name) +{ + struct cpcap_interrupt_desc *d; + int irq, error; + + irq = platform_get_irq_byname(pdev, name); + if (irq < 0) + return irq; + + error = devm_request_threaded_irq(ddata->dev, irq, NULL, + cpcap_battery_irq_thread, + IRQF_SHARED | IRQF_ONESHOT, + name, ddata); + if (error) { + dev_err(ddata->dev, "could not get irq %s: %i\n", + name, error); + + return error; + } + + d = devm_kzalloc(ddata->dev, sizeof(*d), GFP_KERNEL); + if (!d) + return -ENOMEM; + + d->name = name; + d->irq = irq; + + if (!strncmp(name, "cccal", 5)) + d->action = CPCAP_BATTERY_IRQ_ACTION_CC_CAL_DONE; + else if (!strncmp(name, "lowbph", 6)) + d->action = CPCAP_BATTERY_IRQ_ACTION_BATTERY_LOW; + else if (!strncmp(name, "lowbpl", 6)) + d->action = CPCAP_BATTERY_IRQ_ACTION_POWEROFF; + + list_add(&d->node, &ddata->irq_list); + + return 0; +} + +static int cpcap_battery_init_interrupts(struct platform_device *pdev, + struct cpcap_battery_ddata *ddata) +{ + static const char * const cpcap_battery_irqs[] = { + "eol", "lowbph", "lowbpl", + "chrgcurr1", "battdetb" + }; + int i, error; + + for (i = 0; i < ARRAY_SIZE(cpcap_battery_irqs); i++) { + error = cpcap_battery_init_irq(pdev, ddata, + cpcap_battery_irqs[i]); + if (error) + return error; + } + + /* Enable calibration interrupt if already available in dts */ + cpcap_battery_init_irq(pdev, ddata, "cccal"); + + /* Enable low battery interrupts for 3.3V high and 3.1V low */ + error = regmap_update_bits(ddata->reg, CPCAP_REG_BPEOL, + 0xffff, + CPCAP_REG_BPEOL_BIT_BATTDETEN); + if (error) + return error; + + return 0; +} + +static int cpcap_battery_init_iio(struct cpcap_battery_ddata *ddata) +{ + const char * const names[CPCAP_BATTERY_IIO_NR] = { + "battdetb", "battp", "chg_isense", "batti", + }; + int error, i; + + for (i = 0; i < CPCAP_BATTERY_IIO_NR; i++) { + ddata->channels[i] = devm_iio_channel_get(ddata->dev, + names[i]); + if (IS_ERR(ddata->channels[i])) { + error = PTR_ERR(ddata->channels[i]); + goto out_err; + } + + if (!ddata->channels[i]->indio_dev) { + error = -ENXIO; + goto out_err; + } + } + + return 0; + +out_err: + return dev_err_probe(ddata->dev, error, + "could not initialize VBUS or ID IIO\n"); +} + +/* Calibrate coulomb counter */ +static int cpcap_battery_calibrate(struct cpcap_battery_ddata *ddata) +{ + int error, ccc1, value; + unsigned long timeout; + + error = regmap_read(ddata->reg, CPCAP_REG_CCC1, &ccc1); + if (error) + return error; + + timeout = jiffies + msecs_to_jiffies(6000); + + /* Start calibration */ + error = regmap_update_bits(ddata->reg, CPCAP_REG_CCC1, + 0xffff, + CPCAP_REG_CCC1_CAL_EN); + if (error) + goto restore; + + while (time_before(jiffies, timeout)) { + error = regmap_read(ddata->reg, CPCAP_REG_CCC1, &value); + if (error) + goto restore; + + if (!(value & CPCAP_REG_CCC1_CAL_EN)) + break; + + error = regmap_read(ddata->reg, CPCAP_REG_CCM, &value); + if (error) + goto restore; + + msleep(300); + } + + /* Read calibration offset from CCM */ + error = regmap_read(ddata->reg, CPCAP_REG_CCM, &value); + if (error) + goto restore; + + dev_info(ddata->dev, "calibration done: 0x%04x\n", value); + +restore: + if (error) + dev_err(ddata->dev, "%s: error %i\n", __func__, error); + + error = regmap_update_bits(ddata->reg, CPCAP_REG_CCC1, + 0xffff, ccc1); + if (error) + dev_err(ddata->dev, "%s: restore error %i\n", + __func__, error); + + return error; +} + +/* + * Based on the values from Motorola mapphone Linux kernel. In the + * the Motorola mapphone Linux kernel tree the value for pm_cd_factor + * is passed to the kernel via device tree. If it turns out to be + * something device specific we can consider that too later. + * + * And looking at the battery full and shutdown values for the stock + * kernel on droid 4, full is 4351000 and software initiates shutdown + * at 3078000. The device will die around 2743000. + */ +static const struct cpcap_battery_config cpcap_battery_default_data = { + .cd_factor = 0x3cc, + .info.technology = POWER_SUPPLY_TECHNOLOGY_LION, + .info.voltage_max_design = 4351000, + .info.voltage_min_design = 3100000, + .info.charge_full_design = 1740000, + .bat.constant_charge_voltage_max_uv = 4200000, +}; + +#ifdef CONFIG_OF +static const struct of_device_id cpcap_battery_id_table[] = { + { + .compatible = "motorola,cpcap-battery", + .data = &cpcap_battery_default_data, + }, + {}, +}; +MODULE_DEVICE_TABLE(of, cpcap_battery_id_table); +#endif + +static int cpcap_battery_probe(struct platform_device *pdev) +{ + struct power_supply_desc *psy_desc; + struct cpcap_battery_ddata *ddata; + const struct of_device_id *match; + struct power_supply_config psy_cfg = {}; + int error; + + match = of_match_device(of_match_ptr(cpcap_battery_id_table), + &pdev->dev); + if (!match) + return -EINVAL; + + if (!match->data) { + dev_err(&pdev->dev, "no configuration data found\n"); + + return -ENODEV; + } + + ddata = devm_kzalloc(&pdev->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + INIT_LIST_HEAD(&ddata->irq_list); + ddata->dev = &pdev->dev; + memcpy(&ddata->config, match->data, sizeof(ddata->config)); + + ddata->reg = dev_get_regmap(ddata->dev->parent, NULL); + if (!ddata->reg) + return -ENODEV; + + error = cpcap_get_vendor(ddata->dev, ddata->reg, &ddata->vendor); + if (error) + return error; + + switch (ddata->vendor) { + case CPCAP_VENDOR_ST: + ddata->cc_lsb = 95374; /* μAms per LSB */ + break; + case CPCAP_VENDOR_TI: + ddata->cc_lsb = 91501; /* μAms per LSB */ + break; + default: + return -EINVAL; + } + ddata->cc_lsb = (ddata->cc_lsb * ddata->config.cd_factor) / 1000; + + platform_set_drvdata(pdev, ddata); + + error = cpcap_battery_init_interrupts(pdev, ddata); + if (error) + return error; + + error = cpcap_battery_init_iio(ddata); + if (error) + return error; + + psy_desc = devm_kzalloc(ddata->dev, sizeof(*psy_desc), GFP_KERNEL); + if (!psy_desc) + return -ENOMEM; + + psy_desc->name = "battery"; + psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; + psy_desc->properties = cpcap_battery_props; + psy_desc->num_properties = ARRAY_SIZE(cpcap_battery_props); + psy_desc->get_property = cpcap_battery_get_property; + psy_desc->set_property = cpcap_battery_set_property; + psy_desc->property_is_writeable = cpcap_battery_property_is_writeable; + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = ddata; + + ddata->psy = devm_power_supply_register(ddata->dev, psy_desc, + &psy_cfg); + error = PTR_ERR_OR_ZERO(ddata->psy); + if (error) { + dev_err(ddata->dev, "failed to register power supply\n"); + return error; + } + + atomic_set(&ddata->active, 1); + + error = cpcap_battery_calibrate(ddata); + if (error) + return error; + + return 0; +} + +static int cpcap_battery_remove(struct platform_device *pdev) +{ + struct cpcap_battery_ddata *ddata = platform_get_drvdata(pdev); + int error; + + atomic_set(&ddata->active, 0); + error = regmap_update_bits(ddata->reg, CPCAP_REG_BPEOL, + 0xffff, 0); + if (error) + dev_err(&pdev->dev, "could not disable: %i\n", error); + + return 0; +} + +static struct platform_driver cpcap_battery_driver = { + .driver = { + .name = "cpcap_battery", + .of_match_table = of_match_ptr(cpcap_battery_id_table), + }, + .probe = cpcap_battery_probe, + .remove = cpcap_battery_remove, +}; +module_platform_driver(cpcap_battery_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Tony Lindgren <tony@atomide.com>"); +MODULE_DESCRIPTION("CPCAP PMIC Battery Driver"); diff --git a/drivers/power/supply/cpcap-charger.c b/drivers/power/supply/cpcap-charger.c new file mode 100644 index 000000000..891e1eb8e --- /dev/null +++ b/drivers/power/supply/cpcap-charger.c @@ -0,0 +1,927 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Motorola CPCAP PMIC battery charger driver + * + * Copyright (C) 2017 Tony Lindgren <tony@atomide.com> + * + * Rewritten for Linux power framework with some parts based on + * on earlier driver found in the Motorola Linux kernel: + * + * Copyright (C) 2009-2010 Motorola, Inc. + */ + +#include <linux/atomic.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/notifier.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> + +#include <linux/gpio/consumer.h> +#include <linux/usb/phy_companion.h> +#include <linux/phy/omap_usb.h> +#include <linux/usb/otg.h> +#include <linux/iio/consumer.h> +#include <linux/mfd/motorola-cpcap.h> + +/* + * CPCAP_REG_CRM register bits. For documentation of somewhat similar hardware, + * see NXP "MC13783 Power Management and Audio Circuit Users's Guide" + * MC13783UG.pdf chapter "8.5 Battery Interface Register Summary". The registers + * and values for CPCAP are different, but some of the internal components seem + * similar. Also see the Motorola Linux kernel cpcap-regbits.h. CPCAP_REG_CHRGR_1 + * bits that seem to describe the CRM register. + */ +#define CPCAP_REG_CRM_UNUSED_641_15 BIT(15) /* 641 = register number */ +#define CPCAP_REG_CRM_UNUSED_641_14 BIT(14) /* 641 = register number */ +#define CPCAP_REG_CRM_CHRG_LED_EN BIT(13) /* Charger LED */ +#define CPCAP_REG_CRM_RVRSMODE BIT(12) /* USB VBUS output enable */ +#define CPCAP_REG_CRM_ICHRG_TR1 BIT(11) /* Trickle charge current */ +#define CPCAP_REG_CRM_ICHRG_TR0 BIT(10) +#define CPCAP_REG_CRM_FET_OVRD BIT(9) /* 0 = hardware, 1 = FET_CTRL */ +#define CPCAP_REG_CRM_FET_CTRL BIT(8) /* BPFET 1 if FET_OVRD set */ +#define CPCAP_REG_CRM_VCHRG3 BIT(7) /* Charge voltage bits */ +#define CPCAP_REG_CRM_VCHRG2 BIT(6) +#define CPCAP_REG_CRM_VCHRG1 BIT(5) +#define CPCAP_REG_CRM_VCHRG0 BIT(4) +#define CPCAP_REG_CRM_ICHRG3 BIT(3) /* Charge current bits */ +#define CPCAP_REG_CRM_ICHRG2 BIT(2) +#define CPCAP_REG_CRM_ICHRG1 BIT(1) +#define CPCAP_REG_CRM_ICHRG0 BIT(0) + +/* CPCAP_REG_CRM trickle charge voltages */ +#define CPCAP_REG_CRM_TR(val) (((val) & 0x3) << 10) +#define CPCAP_REG_CRM_TR_0A00 CPCAP_REG_CRM_TR(0x0) +#define CPCAP_REG_CRM_TR_0A24 CPCAP_REG_CRM_TR(0x1) +#define CPCAP_REG_CRM_TR_0A48 CPCAP_REG_CRM_TR(0x2) +#define CPCAP_REG_CRM_TR_0A72 CPCAP_REG_CRM_TR(0x4) + +/* + * CPCAP_REG_CRM charge voltages based on the ADC channel 1 values. + * Note that these register bits don't match MC13783UG.pdf VCHRG + * register bits. + */ +#define CPCAP_REG_CRM_VCHRG(val) (((val) & 0xf) << 4) +#define CPCAP_REG_CRM_VCHRG_3V80 CPCAP_REG_CRM_VCHRG(0x0) +#define CPCAP_REG_CRM_VCHRG_4V10 CPCAP_REG_CRM_VCHRG(0x1) +#define CPCAP_REG_CRM_VCHRG_4V12 CPCAP_REG_CRM_VCHRG(0x2) +#define CPCAP_REG_CRM_VCHRG_4V15 CPCAP_REG_CRM_VCHRG(0x3) +#define CPCAP_REG_CRM_VCHRG_4V17 CPCAP_REG_CRM_VCHRG(0x4) +#define CPCAP_REG_CRM_VCHRG_4V20 CPCAP_REG_CRM_VCHRG(0x5) +#define CPCAP_REG_CRM_VCHRG_4V23 CPCAP_REG_CRM_VCHRG(0x6) +#define CPCAP_REG_CRM_VCHRG_4V25 CPCAP_REG_CRM_VCHRG(0x7) +#define CPCAP_REG_CRM_VCHRG_4V27 CPCAP_REG_CRM_VCHRG(0x8) +#define CPCAP_REG_CRM_VCHRG_4V30 CPCAP_REG_CRM_VCHRG(0x9) +#define CPCAP_REG_CRM_VCHRG_4V33 CPCAP_REG_CRM_VCHRG(0xa) +#define CPCAP_REG_CRM_VCHRG_4V35 CPCAP_REG_CRM_VCHRG(0xb) +#define CPCAP_REG_CRM_VCHRG_4V38 CPCAP_REG_CRM_VCHRG(0xc) +#define CPCAP_REG_CRM_VCHRG_4V40 CPCAP_REG_CRM_VCHRG(0xd) +#define CPCAP_REG_CRM_VCHRG_4V42 CPCAP_REG_CRM_VCHRG(0xe) +#define CPCAP_REG_CRM_VCHRG_4V44 CPCAP_REG_CRM_VCHRG(0xf) + +/* + * CPCAP_REG_CRM charge currents. These seem to match MC13783UG.pdf + * values in "Table 8-3. Charge Path Regulator Current Limit + * Characteristics" for the nominal values. + */ +#define CPCAP_REG_CRM_ICHRG(val) (((val) & 0xf) << 0) +#define CPCAP_REG_CRM_ICHRG_0A000 CPCAP_REG_CRM_ICHRG(0x0) +#define CPCAP_REG_CRM_ICHRG_0A070 CPCAP_REG_CRM_ICHRG(0x1) +#define CPCAP_REG_CRM_ICHRG_0A177 CPCAP_REG_CRM_ICHRG(0x2) +#define CPCAP_REG_CRM_ICHRG_0A266 CPCAP_REG_CRM_ICHRG(0x3) +#define CPCAP_REG_CRM_ICHRG_0A355 CPCAP_REG_CRM_ICHRG(0x4) +#define CPCAP_REG_CRM_ICHRG_0A443 CPCAP_REG_CRM_ICHRG(0x5) +#define CPCAP_REG_CRM_ICHRG_0A532 CPCAP_REG_CRM_ICHRG(0x6) +#define CPCAP_REG_CRM_ICHRG_0A621 CPCAP_REG_CRM_ICHRG(0x7) +#define CPCAP_REG_CRM_ICHRG_0A709 CPCAP_REG_CRM_ICHRG(0x8) +#define CPCAP_REG_CRM_ICHRG_0A798 CPCAP_REG_CRM_ICHRG(0x9) +#define CPCAP_REG_CRM_ICHRG_0A886 CPCAP_REG_CRM_ICHRG(0xa) +#define CPCAP_REG_CRM_ICHRG_0A975 CPCAP_REG_CRM_ICHRG(0xb) +#define CPCAP_REG_CRM_ICHRG_1A064 CPCAP_REG_CRM_ICHRG(0xc) +#define CPCAP_REG_CRM_ICHRG_1A152 CPCAP_REG_CRM_ICHRG(0xd) +#define CPCAP_REG_CRM_ICHRG_1A596 CPCAP_REG_CRM_ICHRG(0xe) +#define CPCAP_REG_CRM_ICHRG_NO_LIMIT CPCAP_REG_CRM_ICHRG(0xf) + +/* CPCAP_REG_VUSBC register bits needed for VBUS */ +#define CPCAP_BIT_VBUS_SWITCH BIT(0) /* VBUS boost to 5V */ + +enum { + CPCAP_CHARGER_IIO_BATTDET, + CPCAP_CHARGER_IIO_VOLTAGE, + CPCAP_CHARGER_IIO_VBUS, + CPCAP_CHARGER_IIO_CHRG_CURRENT, + CPCAP_CHARGER_IIO_BATT_CURRENT, + CPCAP_CHARGER_IIO_NR, +}; + +enum { + CPCAP_CHARGER_DISCONNECTED, + CPCAP_CHARGER_DETECTING, + CPCAP_CHARGER_CHARGING, + CPCAP_CHARGER_DONE, +}; + +struct cpcap_charger_ddata { + struct device *dev; + struct regmap *reg; + struct list_head irq_list; + struct delayed_work detect_work; + struct delayed_work vbus_work; + struct gpio_desc *gpio[2]; /* gpio_reven0 & 1 */ + + struct iio_channel *channels[CPCAP_CHARGER_IIO_NR]; + + struct power_supply *usb; + + struct phy_companion comparator; /* For USB VBUS */ + unsigned int vbus_enabled:1; + unsigned int feeding_vbus:1; + atomic_t active; + + int status; + int state; + int voltage; +}; + +struct cpcap_interrupt_desc { + int irq; + struct list_head node; + const char *name; +}; + +struct cpcap_charger_ints_state { + bool chrg_det; + bool rvrs_chrg; + bool vbusov; + + bool chrg_se1b; + bool rvrs_mode; + bool chrgcurr2; + bool chrgcurr1; + bool vbusvld; + + bool battdetb; +}; + +static enum power_supply_property cpcap_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +/* No battery always shows temperature of -40000 */ +static bool cpcap_charger_battery_found(struct cpcap_charger_ddata *ddata) +{ + struct iio_channel *channel; + int error, temperature; + + channel = ddata->channels[CPCAP_CHARGER_IIO_BATTDET]; + error = iio_read_channel_processed(channel, &temperature); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + + return false; + } + + return temperature > -20000 && temperature < 60000; +} + +static int cpcap_charger_get_charge_voltage(struct cpcap_charger_ddata *ddata) +{ + struct iio_channel *channel; + int error, value = 0; + + channel = ddata->channels[CPCAP_CHARGER_IIO_VOLTAGE]; + error = iio_read_channel_processed(channel, &value); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + + return 0; + } + + return value; +} + +static int cpcap_charger_get_charge_current(struct cpcap_charger_ddata *ddata) +{ + struct iio_channel *channel; + int error, value = 0; + + channel = ddata->channels[CPCAP_CHARGER_IIO_CHRG_CURRENT]; + error = iio_read_channel_processed(channel, &value); + if (error < 0) { + dev_warn(ddata->dev, "%s failed: %i\n", __func__, error); + + return 0; + } + + return value; +} + +static int cpcap_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct cpcap_charger_ddata *ddata = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = ddata->status; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + val->intval = ddata->voltage; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (ddata->status == POWER_SUPPLY_STATUS_CHARGING) + val->intval = cpcap_charger_get_charge_voltage(ddata) * + 1000; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (ddata->status == POWER_SUPPLY_STATUS_CHARGING) + val->intval = cpcap_charger_get_charge_current(ddata) * + 1000; + else + val->intval = 0; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = ddata->status == POWER_SUPPLY_STATUS_CHARGING; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int cpcap_charger_match_voltage(int voltage) +{ + switch (voltage) { + case 0 ... 4100000 - 1: return 3800000; + case 4100000 ... 4120000 - 1: return 4100000; + case 4120000 ... 4150000 - 1: return 4120000; + case 4150000 ... 4170000 - 1: return 4150000; + case 4170000 ... 4200000 - 1: return 4170000; + case 4200000 ... 4230000 - 1: return 4200000; + case 4230000 ... 4250000 - 1: return 4230000; + case 4250000 ... 4270000 - 1: return 4250000; + case 4270000 ... 4300000 - 1: return 4270000; + case 4300000 ... 4330000 - 1: return 4300000; + case 4330000 ... 4350000 - 1: return 4330000; + case 4350000 ... 4380000 - 1: return 4350000; + case 4380000 ... 4400000 - 1: return 4380000; + case 4400000 ... 4420000 - 1: return 4400000; + case 4420000 ... 4440000 - 1: return 4420000; + case 4440000: return 4440000; + default: return 0; + } +} + +static int +cpcap_charger_get_bat_const_charge_voltage(struct cpcap_charger_ddata *ddata) +{ + union power_supply_propval prop; + struct power_supply *battery; + int voltage = ddata->voltage; + int error; + + battery = power_supply_get_by_name("battery"); + if (battery) { + error = power_supply_get_property(battery, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + &prop); + if (!error) + voltage = prop.intval; + + power_supply_put(battery); + } + + return voltage; +} + +static int cpcap_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct cpcap_charger_ddata *ddata = dev_get_drvdata(psy->dev.parent); + int voltage, batvolt; + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + voltage = cpcap_charger_match_voltage(val->intval); + batvolt = cpcap_charger_get_bat_const_charge_voltage(ddata); + if (voltage > batvolt) + voltage = batvolt; + ddata->voltage = voltage; + schedule_delayed_work(&ddata->detect_work, 0); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int cpcap_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + return 1; + default: + return 0; + } +} + +static void cpcap_charger_set_cable_path(struct cpcap_charger_ddata *ddata, + bool enabled) +{ + if (!ddata->gpio[0]) + return; + + gpiod_set_value(ddata->gpio[0], enabled); +} + +static void cpcap_charger_set_inductive_path(struct cpcap_charger_ddata *ddata, + bool enabled) +{ + if (!ddata->gpio[1]) + return; + + gpiod_set_value(ddata->gpio[1], enabled); +} + +static int cpcap_charger_set_state(struct cpcap_charger_ddata *ddata, + int max_voltage, int charge_current, + int trickle_current) +{ + bool enable; + int error; + + enable = (charge_current || trickle_current); + dev_dbg(ddata->dev, "%s enable: %i\n", __func__, enable); + + if (!enable) { + error = regmap_update_bits(ddata->reg, CPCAP_REG_CRM, + 0x3fff, + CPCAP_REG_CRM_FET_OVRD | + CPCAP_REG_CRM_FET_CTRL); + if (error) { + ddata->status = POWER_SUPPLY_STATUS_UNKNOWN; + goto out_err; + } + + ddata->status = POWER_SUPPLY_STATUS_DISCHARGING; + + return 0; + } + + error = regmap_update_bits(ddata->reg, CPCAP_REG_CRM, 0x3fff, + CPCAP_REG_CRM_CHRG_LED_EN | + trickle_current | + CPCAP_REG_CRM_FET_OVRD | + CPCAP_REG_CRM_FET_CTRL | + max_voltage | + charge_current); + if (error) { + ddata->status = POWER_SUPPLY_STATUS_UNKNOWN; + goto out_err; + } + + ddata->status = POWER_SUPPLY_STATUS_CHARGING; + + return 0; + +out_err: + dev_err(ddata->dev, "%s failed with %i\n", __func__, error); + + return error; +} + +static bool cpcap_charger_vbus_valid(struct cpcap_charger_ddata *ddata) +{ + int error, value = 0; + struct iio_channel *channel = + ddata->channels[CPCAP_CHARGER_IIO_VBUS]; + + error = iio_read_channel_processed(channel, &value); + if (error >= 0) + return value > 3900 ? true : false; + + dev_err(ddata->dev, "error reading VBUS: %i\n", error); + + return false; +} + +/* VBUS control functions for the USB PHY companion */ +static void cpcap_charger_vbus_work(struct work_struct *work) +{ + struct cpcap_charger_ddata *ddata; + bool vbus = false; + int error; + + ddata = container_of(work, struct cpcap_charger_ddata, + vbus_work.work); + + if (ddata->vbus_enabled) { + vbus = cpcap_charger_vbus_valid(ddata); + if (vbus) { + dev_info(ddata->dev, "VBUS already provided\n"); + + return; + } + + ddata->feeding_vbus = true; + cpcap_charger_set_cable_path(ddata, false); + cpcap_charger_set_inductive_path(ddata, false); + + error = cpcap_charger_set_state(ddata, 0, 0, 0); + if (error) + goto out_err; + + error = regmap_update_bits(ddata->reg, CPCAP_REG_VUSBC, + CPCAP_BIT_VBUS_SWITCH, + CPCAP_BIT_VBUS_SWITCH); + if (error) + goto out_err; + + error = regmap_update_bits(ddata->reg, CPCAP_REG_CRM, + CPCAP_REG_CRM_RVRSMODE, + CPCAP_REG_CRM_RVRSMODE); + if (error) + goto out_err; + } else { + error = regmap_update_bits(ddata->reg, CPCAP_REG_VUSBC, + CPCAP_BIT_VBUS_SWITCH, 0); + if (error) + goto out_err; + + error = regmap_update_bits(ddata->reg, CPCAP_REG_CRM, + CPCAP_REG_CRM_RVRSMODE, 0); + if (error) + goto out_err; + + cpcap_charger_set_cable_path(ddata, true); + cpcap_charger_set_inductive_path(ddata, true); + ddata->feeding_vbus = false; + } + + return; + +out_err: + dev_err(ddata->dev, "%s could not %s vbus: %i\n", __func__, + ddata->vbus_enabled ? "enable" : "disable", error); +} + +static int cpcap_charger_set_vbus(struct phy_companion *comparator, + bool enabled) +{ + struct cpcap_charger_ddata *ddata = + container_of(comparator, struct cpcap_charger_ddata, + comparator); + + ddata->vbus_enabled = enabled; + schedule_delayed_work(&ddata->vbus_work, 0); + + return 0; +} + +/* Charger interrupt handling functions */ + +static int cpcap_charger_get_ints_state(struct cpcap_charger_ddata *ddata, + struct cpcap_charger_ints_state *s) +{ + int val, error; + + error = regmap_read(ddata->reg, CPCAP_REG_INTS1, &val); + if (error) + return error; + + s->chrg_det = val & BIT(13); + s->rvrs_chrg = val & BIT(12); + s->vbusov = val & BIT(11); + + error = regmap_read(ddata->reg, CPCAP_REG_INTS2, &val); + if (error) + return error; + + s->chrg_se1b = val & BIT(13); + s->rvrs_mode = val & BIT(6); + s->chrgcurr2 = val & BIT(5); + s->chrgcurr1 = val & BIT(4); + s->vbusvld = val & BIT(3); + + error = regmap_read(ddata->reg, CPCAP_REG_INTS4, &val); + if (error) + return error; + + s->battdetb = val & BIT(6); + + return 0; +} + +static void cpcap_charger_update_state(struct cpcap_charger_ddata *ddata, + int state) +{ + const char *status; + + if (state > CPCAP_CHARGER_DONE) { + dev_warn(ddata->dev, "unknown state: %i\n", state); + + return; + } + + ddata->state = state; + + switch (state) { + case CPCAP_CHARGER_DISCONNECTED: + status = "DISCONNECTED"; + break; + case CPCAP_CHARGER_DETECTING: + status = "DETECTING"; + break; + case CPCAP_CHARGER_CHARGING: + status = "CHARGING"; + break; + case CPCAP_CHARGER_DONE: + status = "DONE"; + break; + default: + return; + } + + dev_dbg(ddata->dev, "state: %s\n", status); +} + +static int cpcap_charger_voltage_to_regval(int voltage) +{ + int offset; + + switch (voltage) { + case 0 ... 4100000 - 1: + return 0; + case 4100000 ... 4200000 - 1: + offset = 1; + break; + case 4200000 ... 4300000 - 1: + offset = 0; + break; + case 4300000 ... 4380000 - 1: + offset = -1; + break; + case 4380000 ... 4440000: + offset = -2; + break; + default: + return 0; + } + + return ((voltage - 4100000) / 20000) + offset; +} + +static void cpcap_charger_disconnect(struct cpcap_charger_ddata *ddata, + int state, unsigned long delay) +{ + int error; + + error = cpcap_charger_set_state(ddata, 0, 0, 0); + if (error) + return; + + cpcap_charger_update_state(ddata, state); + power_supply_changed(ddata->usb); + schedule_delayed_work(&ddata->detect_work, delay); +} + +static void cpcap_usb_detect(struct work_struct *work) +{ + struct cpcap_charger_ddata *ddata; + struct cpcap_charger_ints_state s; + int error; + + ddata = container_of(work, struct cpcap_charger_ddata, + detect_work.work); + + error = cpcap_charger_get_ints_state(ddata, &s); + if (error) + return; + + /* Just init the state if a charger is connected with no chrg_det set */ + if (!s.chrg_det && s.chrgcurr1 && s.vbusvld) { + cpcap_charger_update_state(ddata, CPCAP_CHARGER_DETECTING); + + return; + } + + /* + * If battery voltage is higher than charge voltage, it may have been + * charged to 4.35V by Android. Try again in 10 minutes. + */ + if (cpcap_charger_get_charge_voltage(ddata) > ddata->voltage) { + cpcap_charger_disconnect(ddata, CPCAP_CHARGER_DETECTING, + HZ * 60 * 10); + + return; + } + + /* Delay for 80ms to avoid vbus bouncing when usb cable is plugged in */ + usleep_range(80000, 120000); + + /* Throttle chrgcurr2 interrupt for charger done and retry */ + switch (ddata->state) { + case CPCAP_CHARGER_CHARGING: + if (s.chrgcurr2) + break; + if (s.chrgcurr1 && s.vbusvld) { + cpcap_charger_disconnect(ddata, CPCAP_CHARGER_DONE, + HZ * 5); + return; + } + break; + case CPCAP_CHARGER_DONE: + if (!s.chrgcurr2) + break; + cpcap_charger_disconnect(ddata, CPCAP_CHARGER_DETECTING, + HZ * 5); + return; + default: + break; + } + + if (!ddata->feeding_vbus && cpcap_charger_vbus_valid(ddata) && + s.chrgcurr1) { + int max_current; + int vchrg; + + if (cpcap_charger_battery_found(ddata)) + max_current = CPCAP_REG_CRM_ICHRG_1A596; + else + max_current = CPCAP_REG_CRM_ICHRG_0A532; + + vchrg = cpcap_charger_voltage_to_regval(ddata->voltage); + error = cpcap_charger_set_state(ddata, + CPCAP_REG_CRM_VCHRG(vchrg), + max_current, 0); + if (error) + goto out_err; + cpcap_charger_update_state(ddata, CPCAP_CHARGER_CHARGING); + } else { + error = cpcap_charger_set_state(ddata, 0, 0, 0); + if (error) + goto out_err; + cpcap_charger_update_state(ddata, CPCAP_CHARGER_DISCONNECTED); + } + + power_supply_changed(ddata->usb); + return; + +out_err: + dev_err(ddata->dev, "%s failed with %i\n", __func__, error); +} + +static irqreturn_t cpcap_charger_irq_thread(int irq, void *data) +{ + struct cpcap_charger_ddata *ddata = data; + + if (!atomic_read(&ddata->active)) + return IRQ_NONE; + + schedule_delayed_work(&ddata->detect_work, 0); + + return IRQ_HANDLED; +} + +static int cpcap_usb_init_irq(struct platform_device *pdev, + struct cpcap_charger_ddata *ddata, + const char *name) +{ + struct cpcap_interrupt_desc *d; + int irq, error; + + irq = platform_get_irq_byname(pdev, name); + if (irq < 0) + return -ENODEV; + + error = devm_request_threaded_irq(ddata->dev, irq, NULL, + cpcap_charger_irq_thread, + IRQF_SHARED | IRQF_ONESHOT, + name, ddata); + if (error) { + dev_err(ddata->dev, "could not get irq %s: %i\n", + name, error); + + return error; + } + + d = devm_kzalloc(ddata->dev, sizeof(*d), GFP_KERNEL); + if (!d) + return -ENOMEM; + + d->name = name; + d->irq = irq; + list_add(&d->node, &ddata->irq_list); + + return 0; +} + +static const char * const cpcap_charger_irqs[] = { + /* REG_INT_0 */ + "chrg_det", "rvrs_chrg", + + /* REG_INT1 */ + "chrg_se1b", "se0conn", "rvrs_mode", "chrgcurr2", "chrgcurr1", "vbusvld", + + /* REG_INT_3 */ + "battdetb", +}; + +static int cpcap_usb_init_interrupts(struct platform_device *pdev, + struct cpcap_charger_ddata *ddata) +{ + int i, error; + + for (i = 0; i < ARRAY_SIZE(cpcap_charger_irqs); i++) { + error = cpcap_usb_init_irq(pdev, ddata, cpcap_charger_irqs[i]); + if (error) + return error; + } + + return 0; +} + +static void cpcap_charger_init_optional_gpios(struct cpcap_charger_ddata *ddata) +{ + int i; + + for (i = 0; i < 2; i++) { + ddata->gpio[i] = devm_gpiod_get_index(ddata->dev, "mode", + i, GPIOD_OUT_HIGH); + if (IS_ERR(ddata->gpio[i])) { + dev_info(ddata->dev, "no mode change GPIO%i: %li\n", + i, PTR_ERR(ddata->gpio[i])); + ddata->gpio[i] = NULL; + } + } +} + +static int cpcap_charger_init_iio(struct cpcap_charger_ddata *ddata) +{ + const char * const names[CPCAP_CHARGER_IIO_NR] = { + "battdetb", "battp", "vbus", "chg_isense", "batti", + }; + int error, i; + + for (i = 0; i < CPCAP_CHARGER_IIO_NR; i++) { + ddata->channels[i] = devm_iio_channel_get(ddata->dev, + names[i]); + if (IS_ERR(ddata->channels[i])) { + error = PTR_ERR(ddata->channels[i]); + goto out_err; + } + + if (!ddata->channels[i]->indio_dev) { + error = -ENXIO; + goto out_err; + } + } + + return 0; + +out_err: + if (error != -EPROBE_DEFER) + dev_err(ddata->dev, "could not initialize VBUS or ID IIO: %i\n", + error); + + return error; +} + +static const struct power_supply_desc cpcap_charger_usb_desc = { + .name = "usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = cpcap_charger_props, + .num_properties = ARRAY_SIZE(cpcap_charger_props), + .get_property = cpcap_charger_get_property, + .set_property = cpcap_charger_set_property, + .property_is_writeable = cpcap_charger_property_is_writeable, +}; + +#ifdef CONFIG_OF +static const struct of_device_id cpcap_charger_id_table[] = { + { + .compatible = "motorola,mapphone-cpcap-charger", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, cpcap_charger_id_table); +#endif + +static int cpcap_charger_probe(struct platform_device *pdev) +{ + struct cpcap_charger_ddata *ddata; + const struct of_device_id *of_id; + struct power_supply_config psy_cfg = {}; + int error; + + of_id = of_match_device(of_match_ptr(cpcap_charger_id_table), + &pdev->dev); + if (!of_id) + return -EINVAL; + + ddata = devm_kzalloc(&pdev->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->dev = &pdev->dev; + ddata->voltage = 4200000; + + ddata->reg = dev_get_regmap(ddata->dev->parent, NULL); + if (!ddata->reg) + return -ENODEV; + + INIT_LIST_HEAD(&ddata->irq_list); + INIT_DELAYED_WORK(&ddata->detect_work, cpcap_usb_detect); + INIT_DELAYED_WORK(&ddata->vbus_work, cpcap_charger_vbus_work); + platform_set_drvdata(pdev, ddata); + + error = cpcap_charger_init_iio(ddata); + if (error) + return error; + + atomic_set(&ddata->active, 1); + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = ddata; + + ddata->usb = devm_power_supply_register(ddata->dev, + &cpcap_charger_usb_desc, + &psy_cfg); + if (IS_ERR(ddata->usb)) { + error = PTR_ERR(ddata->usb); + dev_err(ddata->dev, "failed to register USB charger: %i\n", + error); + + return error; + } + + error = cpcap_usb_init_interrupts(pdev, ddata); + if (error) + return error; + + ddata->comparator.set_vbus = cpcap_charger_set_vbus; + error = omap_usb2_set_comparator(&ddata->comparator); + if (error == -ENODEV) { + dev_info(ddata->dev, "charger needs phy, deferring probe\n"); + return -EPROBE_DEFER; + } + + cpcap_charger_init_optional_gpios(ddata); + + schedule_delayed_work(&ddata->detect_work, 0); + + return 0; +} + +static int cpcap_charger_remove(struct platform_device *pdev) +{ + struct cpcap_charger_ddata *ddata = platform_get_drvdata(pdev); + int error; + + atomic_set(&ddata->active, 0); + error = omap_usb2_set_comparator(NULL); + if (error) + dev_warn(ddata->dev, "could not clear USB comparator: %i\n", + error); + + error = cpcap_charger_set_state(ddata, 0, 0, 0); + if (error) + dev_warn(ddata->dev, "could not clear charger: %i\n", + error); + cancel_delayed_work_sync(&ddata->vbus_work); + cancel_delayed_work_sync(&ddata->detect_work); + + return 0; +} + +static struct platform_driver cpcap_charger_driver = { + .probe = cpcap_charger_probe, + .driver = { + .name = "cpcap-charger", + .of_match_table = of_match_ptr(cpcap_charger_id_table), + }, + .remove = cpcap_charger_remove, +}; +module_platform_driver(cpcap_charger_driver); + +MODULE_AUTHOR("Tony Lindgren <tony@atomide.com>"); +MODULE_DESCRIPTION("CPCAP Battery Charger Interface driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:cpcap-charger"); diff --git a/drivers/power/supply/cros_usbpd-charger.c b/drivers/power/supply/cros_usbpd-charger.c new file mode 100644 index 000000000..0a4f02e4a --- /dev/null +++ b/drivers/power/supply/cros_usbpd-charger.c @@ -0,0 +1,726 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Power supply driver for ChromeOS EC based USB PD Charger. + * + * Copyright (c) 2014 - 2018 Google, Inc + */ + +#include <linux/module.h> +#include <linux/platform_data/cros_ec_commands.h> +#include <linux/platform_data/cros_ec_proto.h> +#include <linux/platform_data/cros_usbpd_notify.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#define CHARGER_USBPD_DIR_NAME "CROS_USBPD_CHARGER%d" +#define CHARGER_DEDICATED_DIR_NAME "CROS_DEDICATED_CHARGER" +#define CHARGER_DIR_NAME_LENGTH (sizeof(CHARGER_USBPD_DIR_NAME) >= \ + sizeof(CHARGER_DEDICATED_DIR_NAME) ? \ + sizeof(CHARGER_USBPD_DIR_NAME) : \ + sizeof(CHARGER_DEDICATED_DIR_NAME)) +#define CHARGER_CACHE_UPDATE_DELAY msecs_to_jiffies(500) +#define CHARGER_MANUFACTURER_MODEL_LENGTH 32 + +#define DRV_NAME "cros-usbpd-charger" + +struct port_data { + int port_number; + char name[CHARGER_DIR_NAME_LENGTH]; + char manufacturer[CHARGER_MANUFACTURER_MODEL_LENGTH]; + char model_name[CHARGER_MANUFACTURER_MODEL_LENGTH]; + struct power_supply *psy; + struct power_supply_desc psy_desc; + int psy_usb_type; + int psy_online; + int psy_status; + int psy_current_max; + int psy_voltage_max_design; + int psy_voltage_now; + int psy_power_max; + struct charger_data *charger; + unsigned long last_update; +}; + +struct charger_data { + struct device *dev; + struct cros_ec_dev *ec_dev; + struct cros_ec_device *ec_device; + int num_charger_ports; + int num_usbpd_ports; + int num_registered_psy; + struct port_data *ports[EC_USB_PD_MAX_PORTS]; + struct notifier_block notifier; +}; + +static enum power_supply_property cros_usbpd_charger_props[] = { + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_USB_TYPE +}; + +static enum power_supply_property cros_usbpd_dedicated_charger_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static enum power_supply_usb_type cros_usbpd_charger_usb_types[] = { + POWER_SUPPLY_USB_TYPE_UNKNOWN, + POWER_SUPPLY_USB_TYPE_SDP, + POWER_SUPPLY_USB_TYPE_DCP, + POWER_SUPPLY_USB_TYPE_CDP, + POWER_SUPPLY_USB_TYPE_C, + POWER_SUPPLY_USB_TYPE_PD, + POWER_SUPPLY_USB_TYPE_PD_DRP, + POWER_SUPPLY_USB_TYPE_APPLE_BRICK_ID +}; + +/* Input voltage/current limit in mV/mA. Default to none. */ +static u16 input_voltage_limit = EC_POWER_LIMIT_NONE; +static u16 input_current_limit = EC_POWER_LIMIT_NONE; + +static bool cros_usbpd_charger_port_is_dedicated(struct port_data *port) +{ + return port->port_number >= port->charger->num_usbpd_ports; +} + +static int cros_usbpd_charger_ec_command(struct charger_data *charger, + unsigned int version, + unsigned int command, + void *outdata, + unsigned int outsize, + void *indata, + unsigned int insize) +{ + struct cros_ec_dev *ec_dev = charger->ec_dev; + struct cros_ec_command *msg; + int ret; + + msg = kzalloc(sizeof(*msg) + max(outsize, insize), GFP_KERNEL); + if (!msg) + return -ENOMEM; + + msg->version = version; + msg->command = ec_dev->cmd_offset + command; + msg->outsize = outsize; + msg->insize = insize; + + if (outsize) + memcpy(msg->data, outdata, outsize); + + ret = cros_ec_cmd_xfer_status(charger->ec_device, msg); + if (ret >= 0 && insize) + memcpy(indata, msg->data, insize); + + kfree(msg); + return ret; +} + +static int cros_usbpd_charger_get_num_ports(struct charger_data *charger) +{ + struct ec_response_charge_port_count resp; + int ret; + + ret = cros_usbpd_charger_ec_command(charger, 0, + EC_CMD_CHARGE_PORT_COUNT, + NULL, 0, &resp, sizeof(resp)); + if (ret < 0) + return ret; + + return resp.port_count; +} + +static int cros_usbpd_charger_get_usbpd_num_ports(struct charger_data *charger) +{ + struct ec_response_usb_pd_ports resp; + int ret; + + ret = cros_usbpd_charger_ec_command(charger, 0, EC_CMD_USB_PD_PORTS, + NULL, 0, &resp, sizeof(resp)); + if (ret < 0) + return ret; + + return resp.num_ports; +} + +static int cros_usbpd_charger_get_discovery_info(struct port_data *port) +{ + struct charger_data *charger = port->charger; + struct ec_params_usb_pd_discovery_entry resp; + struct ec_params_usb_pd_info_request req; + int ret; + + req.port = port->port_number; + + ret = cros_usbpd_charger_ec_command(charger, 0, + EC_CMD_USB_PD_DISCOVERY, + &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) { + dev_err(charger->dev, + "Unable to query discovery info (err:0x%x)\n", ret); + return ret; + } + + dev_dbg(charger->dev, "Port %d: VID = 0x%x, PID=0x%x, PTYPE=0x%x\n", + port->port_number, resp.vid, resp.pid, resp.ptype); + + snprintf(port->manufacturer, sizeof(port->manufacturer), "%x", + resp.vid); + snprintf(port->model_name, sizeof(port->model_name), "%x", resp.pid); + + return 0; +} + +static int cros_usbpd_charger_get_power_info(struct port_data *port) +{ + struct charger_data *charger = port->charger; + struct ec_response_usb_pd_power_info resp; + struct ec_params_usb_pd_power_info req; + int last_psy_status, last_psy_usb_type; + struct device *dev = charger->dev; + int ret; + + req.port = port->port_number; + ret = cros_usbpd_charger_ec_command(charger, 0, + EC_CMD_USB_PD_POWER_INFO, + &req, sizeof(req), + &resp, sizeof(resp)); + if (ret < 0) { + dev_err(dev, "Unable to query PD power info (err:0x%x)\n", ret); + return ret; + } + + last_psy_status = port->psy_status; + last_psy_usb_type = port->psy_usb_type; + + switch (resp.role) { + case USB_PD_PORT_POWER_DISCONNECTED: + port->psy_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + port->psy_online = 0; + break; + case USB_PD_PORT_POWER_SOURCE: + port->psy_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + port->psy_online = 0; + break; + case USB_PD_PORT_POWER_SINK: + port->psy_status = POWER_SUPPLY_STATUS_CHARGING; + port->psy_online = 1; + break; + case USB_PD_PORT_POWER_SINK_NOT_CHARGING: + port->psy_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + port->psy_online = 1; + break; + default: + dev_err(dev, "Unknown role %d\n", resp.role); + break; + } + + port->psy_voltage_max_design = resp.meas.voltage_max; + port->psy_voltage_now = resp.meas.voltage_now; + port->psy_current_max = resp.meas.current_max; + port->psy_power_max = resp.max_power; + + switch (resp.type) { + case USB_CHG_TYPE_BC12_SDP: + case USB_CHG_TYPE_VBUS: + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_SDP; + break; + case USB_CHG_TYPE_NONE: + /* + * For dual-role devices when we are a source, the firmware + * reports the type as NONE. Report such chargers as type + * USB_PD_DRP. + */ + if (resp.role == USB_PD_PORT_POWER_SOURCE && resp.dualrole) + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_PD_DRP; + else + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_SDP; + break; + case USB_CHG_TYPE_OTHER: + case USB_CHG_TYPE_PROPRIETARY: + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_APPLE_BRICK_ID; + break; + case USB_CHG_TYPE_C: + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_C; + break; + case USB_CHG_TYPE_BC12_DCP: + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_DCP; + break; + case USB_CHG_TYPE_BC12_CDP: + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_CDP; + break; + case USB_CHG_TYPE_PD: + if (resp.dualrole) + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_PD_DRP; + else + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_PD; + break; + case USB_CHG_TYPE_UNKNOWN: + /* + * While the EC is trying to determine the type of charger that + * has been plugged in, it will report the charger type as + * unknown. Additionally since the power capabilities are + * unknown, report the max current and voltage as zero. + */ + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + port->psy_voltage_max_design = 0; + port->psy_current_max = 0; + break; + default: + dev_dbg(dev, "Port %d: default case!\n", port->port_number); + port->psy_usb_type = POWER_SUPPLY_USB_TYPE_SDP; + } + + if (cros_usbpd_charger_port_is_dedicated(port)) + port->psy_desc.type = POWER_SUPPLY_TYPE_MAINS; + else + port->psy_desc.type = POWER_SUPPLY_TYPE_USB; + + dev_dbg(dev, + "Port %d: type=%d vmax=%d vnow=%d cmax=%d clim=%d pmax=%d\n", + port->port_number, resp.type, resp.meas.voltage_max, + resp.meas.voltage_now, resp.meas.current_max, + resp.meas.current_lim, resp.max_power); + + /* + * If power supply type or status changed, explicitly call + * power_supply_changed. This results in udev event getting generated + * and allows user mode apps to react quicker instead of waiting for + * their next poll of power supply status. + */ + if (last_psy_usb_type != port->psy_usb_type || + last_psy_status != port->psy_status) + power_supply_changed(port->psy); + + return 0; +} + +static int cros_usbpd_charger_get_port_status(struct port_data *port, + bool ratelimit) +{ + int ret; + + if (ratelimit && + time_is_after_jiffies(port->last_update + + CHARGER_CACHE_UPDATE_DELAY)) + return 0; + + ret = cros_usbpd_charger_get_power_info(port); + if (ret < 0) + return ret; + + if (!cros_usbpd_charger_port_is_dedicated(port)) + ret = cros_usbpd_charger_get_discovery_info(port); + port->last_update = jiffies; + + return ret; +} + +static int cros_usbpd_charger_set_ext_power_limit(struct charger_data *charger, + u16 current_lim, + u16 voltage_lim) +{ + struct ec_params_external_power_limit_v1 req; + int ret; + + req.current_lim = current_lim; + req.voltage_lim = voltage_lim; + + ret = cros_usbpd_charger_ec_command(charger, 0, + EC_CMD_EXTERNAL_POWER_LIMIT, + &req, sizeof(req), NULL, 0); + if (ret < 0) + dev_err(charger->dev, + "Unable to set the 'External Power Limit': %d\n", ret); + + return ret; +} + +static void cros_usbpd_charger_power_changed(struct power_supply *psy) +{ + struct port_data *port = power_supply_get_drvdata(psy); + struct charger_data *charger = port->charger; + int i; + + for (i = 0; i < charger->num_registered_psy; i++) + cros_usbpd_charger_get_port_status(charger->ports[i], false); +} + +static int cros_usbpd_charger_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct port_data *port = power_supply_get_drvdata(psy); + struct charger_data *charger = port->charger; + struct cros_ec_device *ec_device = charger->ec_device; + struct device *dev = charger->dev; + int ret; + + /* Only refresh ec_port_status for dynamic properties */ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + /* + * If mkbp_event_supported, then we can be assured that + * the driver's state for the online property is consistent + * with the hardware. However, if we aren't event driven, + * the optimization before to skip an ec_port_status get + * and only returned cached values of the online property will + * cause a delay in detecting a cable attach until one of the + * other properties are read. + * + * Allow an ec_port_status refresh for online property check + * if we're not already online to check for plug events if + * not mkbp_event_supported. + */ + if (ec_device->mkbp_event_supported || port->psy_online) + break; + fallthrough; + case POWER_SUPPLY_PROP_CURRENT_MAX: + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = cros_usbpd_charger_get_port_status(port, true); + if (ret < 0) { + dev_err(dev, "Failed to get port status (err:0x%x)\n", + ret); + return -EINVAL; + } + break; + default: + break; + } + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = port->psy_online; + break; + case POWER_SUPPLY_PROP_STATUS: + val->intval = port->psy_status; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = port->psy_current_max * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = port->psy_voltage_max_design * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = port->psy_voltage_now * 1000; + break; + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = port->psy_usb_type; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (input_current_limit == EC_POWER_LIMIT_NONE) + val->intval = -1; + else + val->intval = input_current_limit * 1000; + break; + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + if (input_voltage_limit == EC_POWER_LIMIT_NONE) + val->intval = -1; + else + val->intval = input_voltage_limit * 1000; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = port->model_name; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = port->manufacturer; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int cros_usbpd_charger_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct port_data *port = power_supply_get_drvdata(psy); + struct charger_data *charger = port->charger; + struct device *dev = charger->dev; + u16 intval; + int ret; + + /* U16_MAX in mV/mA is the maximum supported value */ + if (val->intval >= U16_MAX * 1000) + return -EINVAL; + /* A negative number is used to clear the limit */ + if (val->intval < 0) + intval = EC_POWER_LIMIT_NONE; + else /* Convert from uA/uV to mA/mV */ + intval = val->intval / 1000; + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = cros_usbpd_charger_set_ext_power_limit(charger, intval, + input_voltage_limit); + if (ret < 0) + break; + + input_current_limit = intval; + if (input_current_limit == EC_POWER_LIMIT_NONE) + dev_info(dev, + "External Current Limit cleared for all ports\n"); + else + dev_info(dev, + "External Current Limit set to %dmA for all ports\n", + input_current_limit); + break; + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + ret = cros_usbpd_charger_set_ext_power_limit(charger, + input_current_limit, + intval); + if (ret < 0) + break; + + input_voltage_limit = intval; + if (input_voltage_limit == EC_POWER_LIMIT_NONE) + dev_info(dev, + "External Voltage Limit cleared for all ports\n"); + else + dev_info(dev, + "External Voltage Limit set to %dmV for all ports\n", + input_voltage_limit); + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static int cros_usbpd_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} + +static int cros_usbpd_charger_ec_event(struct notifier_block *nb, + unsigned long host_event, + void *_notify) +{ + struct charger_data *charger = container_of(nb, struct charger_data, + notifier); + + cros_usbpd_charger_power_changed(charger->ports[0]->psy); + return NOTIFY_OK; +} + +static void cros_usbpd_charger_unregister_notifier(void *data) +{ + struct charger_data *charger = data; + + cros_usbpd_unregister_notify(&charger->notifier); +} + +static int cros_usbpd_charger_probe(struct platform_device *pd) +{ + struct cros_ec_dev *ec_dev = dev_get_drvdata(pd->dev.parent); + struct cros_ec_device *ec_device = ec_dev->ec_dev; + struct power_supply_desc *psy_desc; + struct device *dev = &pd->dev; + struct charger_data *charger; + struct power_supply *psy; + struct port_data *port; + int ret = -EINVAL; + int i; + + charger = devm_kzalloc(dev, sizeof(struct charger_data), + GFP_KERNEL); + if (!charger) + return -ENOMEM; + + charger->dev = dev; + charger->ec_dev = ec_dev; + charger->ec_device = ec_device; + + platform_set_drvdata(pd, charger); + + /* + * We need to know the number of USB PD ports in order to know whether + * there is a dedicated port. The dedicated port will always be + * after the USB PD ports, and there should be only one. + */ + charger->num_usbpd_ports = + cros_usbpd_charger_get_usbpd_num_ports(charger); + if (charger->num_usbpd_ports <= 0) { + /* + * This can happen on a system that doesn't support USB PD. + * Log a message, but no need to warn. + */ + dev_info(dev, "No USB PD charging ports found\n"); + } + + charger->num_charger_ports = cros_usbpd_charger_get_num_ports(charger); + if (charger->num_charger_ports < 0) { + /* + * This can happen on a system that doesn't support USB PD. + * Log a message, but no need to warn. + * Older ECs do not support the above command, in that case + * let's set up the number of charger ports equal to the number + * of USB PD ports + */ + dev_info(dev, "Could not get charger port count\n"); + charger->num_charger_ports = charger->num_usbpd_ports; + } + + if (charger->num_charger_ports <= 0) { + /* + * This can happen on a system that doesn't support USB PD and + * doesn't have a dedicated port. + * Log a message, but no need to warn. + */ + dev_info(dev, "No charging ports found\n"); + ret = -ENODEV; + goto fail_nowarn; + } + + /* + * Sanity checks on the number of ports: + * there should be at most 1 dedicated port + */ + if (charger->num_charger_ports < charger->num_usbpd_ports || + charger->num_charger_ports > (charger->num_usbpd_ports + 1)) { + dev_err(dev, "Unexpected number of charge port count\n"); + ret = -EPROTO; + goto fail_nowarn; + } + + for (i = 0; i < charger->num_charger_ports; i++) { + struct power_supply_config psy_cfg = {}; + + port = devm_kzalloc(dev, sizeof(struct port_data), GFP_KERNEL); + if (!port) { + ret = -ENOMEM; + goto fail; + } + + port->charger = charger; + port->port_number = i; + + psy_desc = &port->psy_desc; + psy_desc->get_property = cros_usbpd_charger_get_prop; + psy_desc->set_property = cros_usbpd_charger_set_prop; + psy_desc->property_is_writeable = + cros_usbpd_charger_property_is_writeable; + psy_desc->external_power_changed = + cros_usbpd_charger_power_changed; + psy_cfg.drv_data = port; + + if (cros_usbpd_charger_port_is_dedicated(port)) { + sprintf(port->name, CHARGER_DEDICATED_DIR_NAME); + psy_desc->type = POWER_SUPPLY_TYPE_MAINS; + psy_desc->properties = + cros_usbpd_dedicated_charger_props; + psy_desc->num_properties = + ARRAY_SIZE(cros_usbpd_dedicated_charger_props); + } else { + sprintf(port->name, CHARGER_USBPD_DIR_NAME, i); + psy_desc->type = POWER_SUPPLY_TYPE_USB; + psy_desc->properties = cros_usbpd_charger_props; + psy_desc->num_properties = + ARRAY_SIZE(cros_usbpd_charger_props); + psy_desc->usb_types = cros_usbpd_charger_usb_types; + psy_desc->num_usb_types = + ARRAY_SIZE(cros_usbpd_charger_usb_types); + } + + psy_desc->name = port->name; + + psy = devm_power_supply_register_no_ws(dev, psy_desc, + &psy_cfg); + if (IS_ERR(psy)) { + dev_err(dev, "Failed to register power supply\n"); + continue; + } + port->psy = psy; + + charger->ports[charger->num_registered_psy++] = port; + } + + if (!charger->num_registered_psy) { + ret = -ENODEV; + dev_err(dev, "No power supplies registered\n"); + goto fail; + } + + /* Get PD events from the EC */ + charger->notifier.notifier_call = cros_usbpd_charger_ec_event; + ret = cros_usbpd_register_notify(&charger->notifier); + if (ret < 0) { + dev_warn(dev, "failed to register notifier\n"); + } else { + ret = devm_add_action_or_reset(dev, + cros_usbpd_charger_unregister_notifier, + charger); + if (ret < 0) + goto fail; + } + + return 0; + +fail: + WARN(1, "%s: Failing probe (err:0x%x)\n", dev_name(dev), ret); + +fail_nowarn: + dev_info(dev, "Failing probe (err:0x%x)\n", ret); + return ret; +} + +#ifdef CONFIG_PM_SLEEP +static int cros_usbpd_charger_resume(struct device *dev) +{ + struct charger_data *charger = dev_get_drvdata(dev); + int i; + + if (!charger) + return 0; + + for (i = 0; i < charger->num_registered_psy; i++) { + power_supply_changed(charger->ports[i]->psy); + charger->ports[i]->last_update = + jiffies - CHARGER_CACHE_UPDATE_DELAY; + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(cros_usbpd_charger_pm_ops, NULL, + cros_usbpd_charger_resume); + +static struct platform_driver cros_usbpd_charger_driver = { + .driver = { + .name = DRV_NAME, + .pm = &cros_usbpd_charger_pm_ops, + }, + .probe = cros_usbpd_charger_probe +}; + +module_platform_driver(cros_usbpd_charger_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("ChromeOS EC USBPD charger"); +MODULE_ALIAS("platform:" DRV_NAME); diff --git a/drivers/power/supply/cw2015_battery.c b/drivers/power/supply/cw2015_battery.c new file mode 100644 index 000000000..d1071dbb9 --- /dev/null +++ b/drivers/power/supply/cw2015_battery.c @@ -0,0 +1,752 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Fuel gauge driver for CellWise 2013 / 2015 + * + * Copyright (C) 2012, RockChip + * Copyright (C) 2020, Tobias Schramm + * + * Authors: xuhuicong <xhc@rock-chips.com> + * Authors: Tobias Schramm <t.schramm@manjaro.org> + */ + +#include <linux/bits.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/gfp.h> +#include <linux/gpio/consumer.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/regmap.h> +#include <linux/time.h> +#include <linux/workqueue.h> + +#define CW2015_SIZE_BATINFO 64 + +#define CW2015_RESET_TRIES 5 + +#define CW2015_REG_VERSION 0x00 +#define CW2015_REG_VCELL 0x02 +#define CW2015_REG_SOC 0x04 +#define CW2015_REG_RRT_ALERT 0x06 +#define CW2015_REG_CONFIG 0x08 +#define CW2015_REG_MODE 0x0A +#define CW2015_REG_BATINFO 0x10 + +#define CW2015_MODE_SLEEP_MASK GENMASK(7, 6) +#define CW2015_MODE_SLEEP (0x03 << 6) +#define CW2015_MODE_NORMAL (0x00 << 6) +#define CW2015_MODE_QUICK_START (0x03 << 4) +#define CW2015_MODE_RESTART (0x0f << 0) + +#define CW2015_CONFIG_UPDATE_FLG (0x01 << 1) +#define CW2015_ATHD(x) ((x) << 3) +#define CW2015_MASK_ATHD GENMASK(7, 3) +#define CW2015_MASK_SOC GENMASK(12, 0) + +/* reset gauge of no valid state of charge could be polled for 40s */ +#define CW2015_BAT_SOC_ERROR_MS (40 * MSEC_PER_SEC) +/* reset gauge if state of charge stuck for half an hour during charging */ +#define CW2015_BAT_CHARGING_STUCK_MS (1800 * MSEC_PER_SEC) + +/* poll interval from CellWise GPL Android driver example */ +#define CW2015_DEFAULT_POLL_INTERVAL_MS 8000 + +#define CW2015_AVERAGING_SAMPLES 3 + +struct cw_battery { + struct device *dev; + struct workqueue_struct *battery_workqueue; + struct delayed_work battery_delay_work; + struct regmap *regmap; + struct power_supply *rk_bat; + struct power_supply_battery_info battery; + u8 *bat_profile; + + bool charger_attached; + bool battery_changed; + + int soc; + int voltage_mv; + int status; + int time_to_empty; + int charge_count; + + u32 poll_interval_ms; + u8 alert_level; + + unsigned int read_errors; + unsigned int charge_stuck_cnt; +}; + +static int cw_read_word(struct cw_battery *cw_bat, u8 reg, u16 *val) +{ + __be16 value; + int ret; + + ret = regmap_bulk_read(cw_bat->regmap, reg, &value, sizeof(value)); + if (ret) + return ret; + + *val = be16_to_cpu(value); + return 0; +} + +static int cw_update_profile(struct cw_battery *cw_bat) +{ + int ret; + unsigned int reg_val; + u8 reset_val; + + /* make sure gauge is not in sleep mode */ + ret = regmap_read(cw_bat->regmap, CW2015_REG_MODE, ®_val); + if (ret) + return ret; + + reset_val = reg_val; + if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) { + dev_err(cw_bat->dev, + "Gauge is in sleep mode, can't update battery info\n"); + return -EINVAL; + } + + /* write new battery info */ + ret = regmap_raw_write(cw_bat->regmap, CW2015_REG_BATINFO, + cw_bat->bat_profile, + CW2015_SIZE_BATINFO); + if (ret) + return ret; + + /* set config update flag */ + reg_val |= CW2015_CONFIG_UPDATE_FLG; + reg_val &= ~CW2015_MASK_ATHD; + reg_val |= CW2015_ATHD(cw_bat->alert_level); + ret = regmap_write(cw_bat->regmap, CW2015_REG_CONFIG, reg_val); + if (ret) + return ret; + + /* reset gauge to apply new battery profile */ + reset_val &= ~CW2015_MODE_RESTART; + reg_val = reset_val | CW2015_MODE_RESTART; + ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reg_val); + if (ret) + return ret; + + /* wait for gauge to reset */ + msleep(20); + + /* clear reset flag */ + ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val); + if (ret) + return ret; + + /* wait for gauge to become ready */ + ret = regmap_read_poll_timeout(cw_bat->regmap, CW2015_REG_SOC, + reg_val, reg_val <= 100, + 10 * USEC_PER_MSEC, 10 * USEC_PER_SEC); + if (ret) + dev_err(cw_bat->dev, + "Gauge did not become ready after profile upload\n"); + else + dev_dbg(cw_bat->dev, "Battery profile updated\n"); + + return ret; +} + +static int cw_init(struct cw_battery *cw_bat) +{ + int ret; + unsigned int reg_val = CW2015_MODE_SLEEP; + + if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) { + reg_val = CW2015_MODE_NORMAL; + ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reg_val); + if (ret) + return ret; + } + + ret = regmap_read(cw_bat->regmap, CW2015_REG_CONFIG, ®_val); + if (ret) + return ret; + + if ((reg_val & CW2015_MASK_ATHD) != CW2015_ATHD(cw_bat->alert_level)) { + dev_dbg(cw_bat->dev, "Setting new alert level\n"); + reg_val &= ~CW2015_MASK_ATHD; + reg_val |= ~CW2015_ATHD(cw_bat->alert_level); + ret = regmap_write(cw_bat->regmap, CW2015_REG_CONFIG, reg_val); + if (ret) + return ret; + } + + ret = regmap_read(cw_bat->regmap, CW2015_REG_CONFIG, ®_val); + if (ret) + return ret; + + if (!(reg_val & CW2015_CONFIG_UPDATE_FLG)) { + dev_dbg(cw_bat->dev, + "Battery profile not present, uploading battery profile\n"); + if (cw_bat->bat_profile) { + ret = cw_update_profile(cw_bat); + if (ret) { + dev_err(cw_bat->dev, + "Failed to upload battery profile\n"); + return ret; + } + } else { + dev_warn(cw_bat->dev, + "No profile specified, continuing without profile\n"); + } + } else if (cw_bat->bat_profile) { + u8 bat_info[CW2015_SIZE_BATINFO]; + + ret = regmap_raw_read(cw_bat->regmap, CW2015_REG_BATINFO, + bat_info, CW2015_SIZE_BATINFO); + if (ret) { + dev_err(cw_bat->dev, + "Failed to read stored battery profile\n"); + return ret; + } + + if (memcmp(bat_info, cw_bat->bat_profile, CW2015_SIZE_BATINFO)) { + dev_warn(cw_bat->dev, "Replacing stored battery profile\n"); + ret = cw_update_profile(cw_bat); + if (ret) + return ret; + } + } else { + dev_warn(cw_bat->dev, + "Can't check current battery profile, no profile provided\n"); + } + + dev_dbg(cw_bat->dev, "Battery profile configured\n"); + return 0; +} + +static int cw_power_on_reset(struct cw_battery *cw_bat) +{ + int ret; + unsigned char reset_val; + + reset_val = CW2015_MODE_SLEEP; + ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val); + if (ret) + return ret; + + /* wait for gauge to enter sleep */ + msleep(20); + + reset_val = CW2015_MODE_NORMAL; + ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val); + if (ret) + return ret; + + ret = cw_init(cw_bat); + if (ret) + return ret; + return 0; +} + +#define HYSTERESIS(current, previous, up, down) \ + (((current) < (previous) + (up)) && ((current) > (previous) - (down))) + +static int cw_get_soc(struct cw_battery *cw_bat) +{ + unsigned int soc; + int ret; + + ret = regmap_read(cw_bat->regmap, CW2015_REG_SOC, &soc); + if (ret) + return ret; + + if (soc > 100) { + int max_error_cycles = + CW2015_BAT_SOC_ERROR_MS / cw_bat->poll_interval_ms; + + dev_err(cw_bat->dev, "Invalid SoC %d%%\n", soc); + cw_bat->read_errors++; + if (cw_bat->read_errors > max_error_cycles) { + dev_warn(cw_bat->dev, + "Too many invalid SoC reports, resetting gauge\n"); + cw_power_on_reset(cw_bat); + cw_bat->read_errors = 0; + } + return cw_bat->soc; + } + cw_bat->read_errors = 0; + + /* Reset gauge if stuck while charging */ + if (cw_bat->status == POWER_SUPPLY_STATUS_CHARGING && soc == cw_bat->soc) { + int max_stuck_cycles = + CW2015_BAT_CHARGING_STUCK_MS / cw_bat->poll_interval_ms; + + cw_bat->charge_stuck_cnt++; + if (cw_bat->charge_stuck_cnt > max_stuck_cycles) { + dev_warn(cw_bat->dev, + "SoC stuck @%u%%, resetting gauge\n", soc); + cw_power_on_reset(cw_bat); + cw_bat->charge_stuck_cnt = 0; + } + } else { + cw_bat->charge_stuck_cnt = 0; + } + + /* Ignore voltage dips during charge */ + if (cw_bat->charger_attached && HYSTERESIS(soc, cw_bat->soc, 0, 3)) + soc = cw_bat->soc; + + /* Ignore voltage spikes during discharge */ + if (!cw_bat->charger_attached && HYSTERESIS(soc, cw_bat->soc, 3, 0)) + soc = cw_bat->soc; + + return soc; +} + +static int cw_get_voltage(struct cw_battery *cw_bat) +{ + int ret, i, voltage_mv; + u16 reg_val; + u32 avg = 0; + + for (i = 0; i < CW2015_AVERAGING_SAMPLES; i++) { + ret = cw_read_word(cw_bat, CW2015_REG_VCELL, ®_val); + if (ret) + return ret; + + avg += reg_val; + } + avg /= CW2015_AVERAGING_SAMPLES; + + /* + * 305 uV per ADC step + * Use 312 / 1024 as efficient approximation of 305 / 1000 + * Negligible error of 0.1% + */ + voltage_mv = avg * 312 / 1024; + + dev_dbg(cw_bat->dev, "Read voltage: %d mV, raw=0x%04x\n", + voltage_mv, reg_val); + return voltage_mv; +} + +static int cw_get_time_to_empty(struct cw_battery *cw_bat) +{ + int ret; + u16 value16; + + ret = cw_read_word(cw_bat, CW2015_REG_RRT_ALERT, &value16); + if (ret) + return ret; + + return value16 & CW2015_MASK_SOC; +} + +static void cw_update_charge_status(struct cw_battery *cw_bat) +{ + int ret; + + ret = power_supply_am_i_supplied(cw_bat->rk_bat); + if (ret < 0) { + dev_warn(cw_bat->dev, "Failed to get supply state: %d\n", ret); + } else { + bool charger_attached; + + charger_attached = !!ret; + if (cw_bat->charger_attached != charger_attached) { + cw_bat->battery_changed = true; + if (charger_attached) + cw_bat->charge_count++; + } + cw_bat->charger_attached = charger_attached; + } +} + +static void cw_update_soc(struct cw_battery *cw_bat) +{ + int soc; + + soc = cw_get_soc(cw_bat); + if (soc < 0) + dev_err(cw_bat->dev, "Failed to get SoC from gauge: %d\n", soc); + else if (cw_bat->soc != soc) { + cw_bat->soc = soc; + cw_bat->battery_changed = true; + } +} + +static void cw_update_voltage(struct cw_battery *cw_bat) +{ + int voltage_mv; + + voltage_mv = cw_get_voltage(cw_bat); + if (voltage_mv < 0) + dev_err(cw_bat->dev, "Failed to get voltage from gauge: %d\n", + voltage_mv); + else + cw_bat->voltage_mv = voltage_mv; +} + +static void cw_update_status(struct cw_battery *cw_bat) +{ + int status = POWER_SUPPLY_STATUS_DISCHARGING; + + if (cw_bat->charger_attached) { + if (cw_bat->soc >= 100) + status = POWER_SUPPLY_STATUS_FULL; + else + status = POWER_SUPPLY_STATUS_CHARGING; + } + + if (cw_bat->status != status) + cw_bat->battery_changed = true; + cw_bat->status = status; +} + +static void cw_update_time_to_empty(struct cw_battery *cw_bat) +{ + int time_to_empty; + + time_to_empty = cw_get_time_to_empty(cw_bat); + if (time_to_empty < 0) + dev_err(cw_bat->dev, "Failed to get time to empty from gauge: %d\n", + time_to_empty); + else if (cw_bat->time_to_empty != time_to_empty) { + cw_bat->time_to_empty = time_to_empty; + cw_bat->battery_changed = true; + } +} + +static void cw_bat_work(struct work_struct *work) +{ + struct delayed_work *delay_work; + struct cw_battery *cw_bat; + int ret; + unsigned int reg_val; + + delay_work = to_delayed_work(work); + cw_bat = container_of(delay_work, struct cw_battery, battery_delay_work); + ret = regmap_read(cw_bat->regmap, CW2015_REG_MODE, ®_val); + if (ret) { + dev_err(cw_bat->dev, "Failed to read mode from gauge: %d\n", ret); + } else { + if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) { + int i; + + for (i = 0; i < CW2015_RESET_TRIES; i++) { + if (!cw_power_on_reset(cw_bat)) + break; + } + } + cw_update_soc(cw_bat); + cw_update_voltage(cw_bat); + cw_update_charge_status(cw_bat); + cw_update_status(cw_bat); + cw_update_time_to_empty(cw_bat); + } + dev_dbg(cw_bat->dev, "charger_attached = %d\n", cw_bat->charger_attached); + dev_dbg(cw_bat->dev, "status = %d\n", cw_bat->status); + dev_dbg(cw_bat->dev, "soc = %d%%\n", cw_bat->soc); + dev_dbg(cw_bat->dev, "voltage = %dmV\n", cw_bat->voltage_mv); + + if (cw_bat->battery_changed) + power_supply_changed(cw_bat->rk_bat); + cw_bat->battery_changed = false; + + queue_delayed_work(cw_bat->battery_workqueue, + &cw_bat->battery_delay_work, + msecs_to_jiffies(cw_bat->poll_interval_ms)); +} + +static bool cw_battery_valid_time_to_empty(struct cw_battery *cw_bat) +{ + return cw_bat->time_to_empty > 0 && + cw_bat->time_to_empty < CW2015_MASK_SOC && + cw_bat->status == POWER_SUPPLY_STATUS_DISCHARGING; +} + +static int cw_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct cw_battery *cw_bat; + + cw_bat = power_supply_get_drvdata(psy); + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = cw_bat->soc; + break; + + case POWER_SUPPLY_PROP_STATUS: + val->intval = cw_bat->status; + break; + + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!cw_bat->voltage_mv; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = cw_bat->voltage_mv * 1000; + break; + + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + if (cw_battery_valid_time_to_empty(cw_bat)) + val->intval = cw_bat->time_to_empty * 60; + else + val->intval = 0; + break; + + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + val->intval = cw_bat->charge_count; + break; + + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + if (cw_bat->battery.charge_full_design_uah > 0) + val->intval = cw_bat->battery.charge_full_design_uah; + else + val->intval = 0; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (cw_battery_valid_time_to_empty(cw_bat) && + cw_bat->battery.charge_full_design_uah > 0) { + /* calculate remaining capacity */ + val->intval = cw_bat->battery.charge_full_design_uah; + val->intval = val->intval * cw_bat->soc / 100; + + /* estimate current based on time to empty */ + val->intval = 60 * val->intval / cw_bat->time_to_empty; + } else { + val->intval = 0; + } + + break; + + default: + break; + } + return 0; +} + +static enum power_supply_property cw_battery_properties[] = { + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static const struct power_supply_desc cw2015_bat_desc = { + .name = "cw2015-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = cw_battery_properties, + .num_properties = ARRAY_SIZE(cw_battery_properties), + .get_property = cw_battery_get_property, +}; + +static int cw2015_parse_properties(struct cw_battery *cw_bat) +{ + struct device *dev = cw_bat->dev; + int length; + int ret; + + 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 != CW2015_SIZE_BATINFO) { + dev_err(cw_bat->dev, "battery-profile must be %d bytes\n", + CW2015_SIZE_BATINFO); + 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; + } + + 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 = CW2015_DEFAULT_POLL_INTERVAL_MS; + } + + return 0; +} + +static const struct regmap_range regmap_ranges_rd_yes[] = { + regmap_reg_range(CW2015_REG_VERSION, CW2015_REG_VERSION), + regmap_reg_range(CW2015_REG_VCELL, CW2015_REG_CONFIG), + regmap_reg_range(CW2015_REG_MODE, CW2015_REG_MODE), + regmap_reg_range(CW2015_REG_BATINFO, + CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1), +}; + +static const struct regmap_access_table regmap_rd_table = { + .yes_ranges = regmap_ranges_rd_yes, + .n_yes_ranges = 4, +}; + +static const struct regmap_range regmap_ranges_wr_yes[] = { + regmap_reg_range(CW2015_REG_RRT_ALERT, CW2015_REG_CONFIG), + regmap_reg_range(CW2015_REG_MODE, CW2015_REG_MODE), + regmap_reg_range(CW2015_REG_BATINFO, + CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1), +}; + +static const struct regmap_access_table regmap_wr_table = { + .yes_ranges = regmap_ranges_wr_yes, + .n_yes_ranges = 3, +}; + +static const struct regmap_range regmap_ranges_vol_yes[] = { + regmap_reg_range(CW2015_REG_VCELL, CW2015_REG_SOC + 1), +}; + +static const struct regmap_access_table regmap_vol_table = { + .yes_ranges = regmap_ranges_vol_yes, + .n_yes_ranges = 1, +}; + +static const struct regmap_config cw2015_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .rd_table = ®map_rd_table, + .wr_table = ®map_wr_table, + .volatile_table = ®map_vol_table, + .max_register = CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1, +}; + +static int cw_bat_probe(struct i2c_client *client) +{ + int ret; + struct cw_battery *cw_bat; + struct power_supply_config psy_cfg = { 0 }; + + 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->soc = 1; + + ret = cw2015_parse_properties(cw_bat); + if (ret) { + dev_err(cw_bat->dev, "Failed to parse cw2015 properties\n"); + return ret; + } + + cw_bat->regmap = devm_regmap_init_i2c(client, &cw2015_regmap_config); + if (IS_ERR(cw_bat->regmap)) { + dev_err(cw_bat->dev, "Failed to allocate regmap: %ld\n", + PTR_ERR(cw_bat->regmap)); + return PTR_ERR(cw_bat->regmap); + } + + ret = cw_init(cw_bat); + if (ret) { + dev_err(cw_bat->dev, "Init failed: %d\n", ret); + return ret; + } + + psy_cfg.drv_data = cw_bat; + psy_cfg.fwnode = dev_fwnode(cw_bat->dev); + + cw_bat->rk_bat = devm_power_supply_register(&client->dev, + &cw2015_bat_desc, + &psy_cfg); + if (IS_ERR(cw_bat->rk_bat)) { + /* try again if this happens */ + dev_err_probe(&client->dev, PTR_ERR(cw_bat->rk_bat), + "Failed to register power supply\n"); + return PTR_ERR(cw_bat->rk_bat); + } + + ret = power_supply_get_battery_info(cw_bat->rk_bat, &cw_bat->battery); + if (ret) { + dev_warn(cw_bat->dev, + "No monitored battery, some properties will be missing\n"); + } + + cw_bat->battery_workqueue = create_singlethread_workqueue("rk_battery"); + INIT_DELAYED_WORK(&cw_bat->battery_delay_work, cw_bat_work); + queue_delayed_work(cw_bat->battery_workqueue, + &cw_bat->battery_delay_work, msecs_to_jiffies(10)); + return 0; +} + +static int __maybe_unused cw_bat_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct cw_battery *cw_bat = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&cw_bat->battery_delay_work); + return 0; +} + +static int __maybe_unused cw_bat_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct cw_battery *cw_bat = i2c_get_clientdata(client); + + queue_delayed_work(cw_bat->battery_workqueue, + &cw_bat->battery_delay_work, 0); + return 0; +} + +static SIMPLE_DEV_PM_OPS(cw_bat_pm_ops, cw_bat_suspend, cw_bat_resume); + +static int cw_bat_remove(struct i2c_client *client) +{ + struct cw_battery *cw_bat = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&cw_bat->battery_delay_work); + power_supply_put_battery_info(cw_bat->rk_bat, &cw_bat->battery); + return 0; +} + +static const struct i2c_device_id cw_bat_id_table[] = { + { "cw2015", 0 }, + { } +}; + +static const struct of_device_id cw2015_of_match[] = { + { .compatible = "cellwise,cw2015" }, + { } +}; +MODULE_DEVICE_TABLE(of, cw2015_of_match); + +static struct i2c_driver cw_bat_driver = { + .driver = { + .name = "cw2015", + .of_match_table = cw2015_of_match, + .pm = &cw_bat_pm_ops, + }, + .probe_new = cw_bat_probe, + .remove = cw_bat_remove, + .id_table = cw_bat_id_table, +}; + +module_i2c_driver(cw_bat_driver); + +MODULE_AUTHOR("xhc<xhc@rock-chips.com>"); +MODULE_AUTHOR("Tobias Schramm <t.schramm@manjaro.org>"); +MODULE_DESCRIPTION("cw2015/cw2013 battery driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/da9030_battery.c b/drivers/power/supply/da9030_battery.c new file mode 100644 index 000000000..0deba48d2 --- /dev/null +++ b/drivers/power/supply/da9030_battery.c @@ -0,0 +1,583 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery charger driver for Dialog Semiconductor DA9030 + * + * Copyright (C) 2008 Compulab, Ltd. + * Mike Rapoport <mike@compulab.co.il> + */ + +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/device.h> +#include <linux/workqueue.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/da903x.h> + +#include <linux/debugfs.h> +#include <linux/seq_file.h> +#include <linux/notifier.h> + +#define DA9030_FAULT_LOG 0x0a +#define DA9030_FAULT_LOG_OVER_TEMP (1 << 7) +#define DA9030_FAULT_LOG_VBAT_OVER (1 << 4) + +#define DA9030_CHARGE_CONTROL 0x28 +#define DA9030_CHRG_CHARGER_ENABLE (1 << 7) + +#define DA9030_ADC_MAN_CONTROL 0x30 +#define DA9030_ADC_TBATREF_ENABLE (1 << 5) +#define DA9030_ADC_LDO_INT_ENABLE (1 << 4) + +#define DA9030_ADC_AUTO_CONTROL 0x31 +#define DA9030_ADC_TBAT_ENABLE (1 << 5) +#define DA9030_ADC_VBAT_IN_TXON (1 << 4) +#define DA9030_ADC_VCH_ENABLE (1 << 3) +#define DA9030_ADC_ICH_ENABLE (1 << 2) +#define DA9030_ADC_VBAT_ENABLE (1 << 1) +#define DA9030_ADC_AUTO_SLEEP_ENABLE (1 << 0) + +#define DA9030_VBATMON 0x32 +#define DA9030_VBATMONTXON 0x33 +#define DA9030_TBATHIGHP 0x34 +#define DA9030_TBATHIGHN 0x35 +#define DA9030_TBATLOW 0x36 + +#define DA9030_VBAT_RES 0x41 +#define DA9030_VBATMIN_RES 0x42 +#define DA9030_VBATMINTXON_RES 0x43 +#define DA9030_ICHMAX_RES 0x44 +#define DA9030_ICHMIN_RES 0x45 +#define DA9030_ICHAVERAGE_RES 0x46 +#define DA9030_VCHMAX_RES 0x47 +#define DA9030_VCHMIN_RES 0x48 +#define DA9030_TBAT_RES 0x49 + +struct da9030_adc_res { + uint8_t vbat_res; + uint8_t vbatmin_res; + uint8_t vbatmintxon; + uint8_t ichmax_res; + uint8_t ichmin_res; + uint8_t ichaverage_res; + uint8_t vchmax_res; + uint8_t vchmin_res; + uint8_t tbat_res; + uint8_t adc_in4_res; + uint8_t adc_in5_res; +}; + +struct da9030_battery_thresholds { + int tbat_low; + int tbat_high; + int tbat_restart; + + int vbat_low; + int vbat_crit; + int vbat_charge_start; + int vbat_charge_stop; + int vbat_charge_restart; + + int vcharge_min; + int vcharge_max; +}; + +struct da9030_charger { + struct power_supply *psy; + struct power_supply_desc psy_desc; + + struct device *master; + + struct da9030_adc_res adc; + struct delayed_work work; + unsigned int interval; + + struct power_supply_info *battery_info; + + struct da9030_battery_thresholds thresholds; + + unsigned int charge_milliamp; + unsigned int charge_millivolt; + + /* charger status */ + bool chdet; + uint8_t fault; + int mA; + int mV; + bool is_on; + + struct notifier_block nb; + + /* platform callbacks for battery low and critical events */ + void (*battery_low)(void); + void (*battery_critical)(void); + + struct dentry *debug_file; +}; + +static inline int da9030_reg_to_mV(int reg) +{ + return ((reg * 2650) >> 8) + 2650; +} + +static inline int da9030_millivolt_to_reg(int mV) +{ + return ((mV - 2650) << 8) / 2650; +} + +static inline int da9030_reg_to_mA(int reg) +{ + return ((reg * 24000) >> 8) / 15; +} + +#ifdef CONFIG_DEBUG_FS +static int bat_debug_show(struct seq_file *s, void *data) +{ + struct da9030_charger *charger = s->private; + + seq_printf(s, "charger is %s\n", charger->is_on ? "on" : "off"); + if (charger->chdet) { + seq_printf(s, "iset = %dmA, vset = %dmV\n", + charger->mA, charger->mV); + } + + seq_printf(s, "vbat_res = %d (%dmV)\n", + charger->adc.vbat_res, + da9030_reg_to_mV(charger->adc.vbat_res)); + seq_printf(s, "vbatmin_res = %d (%dmV)\n", + charger->adc.vbatmin_res, + da9030_reg_to_mV(charger->adc.vbatmin_res)); + seq_printf(s, "vbatmintxon = %d (%dmV)\n", + charger->adc.vbatmintxon, + da9030_reg_to_mV(charger->adc.vbatmintxon)); + seq_printf(s, "ichmax_res = %d (%dmA)\n", + charger->adc.ichmax_res, + da9030_reg_to_mV(charger->adc.ichmax_res)); + seq_printf(s, "ichmin_res = %d (%dmA)\n", + charger->adc.ichmin_res, + da9030_reg_to_mA(charger->adc.ichmin_res)); + seq_printf(s, "ichaverage_res = %d (%dmA)\n", + charger->adc.ichaverage_res, + da9030_reg_to_mA(charger->adc.ichaverage_res)); + seq_printf(s, "vchmax_res = %d (%dmV)\n", + charger->adc.vchmax_res, + da9030_reg_to_mA(charger->adc.vchmax_res)); + seq_printf(s, "vchmin_res = %d (%dmV)\n", + charger->adc.vchmin_res, + da9030_reg_to_mV(charger->adc.vchmin_res)); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(bat_debug); + +static struct dentry *da9030_bat_create_debugfs(struct da9030_charger *charger) +{ + charger->debug_file = debugfs_create_file("charger", 0666, NULL, + charger, &bat_debug_fops); + return charger->debug_file; +} + +static void da9030_bat_remove_debugfs(struct da9030_charger *charger) +{ + debugfs_remove(charger->debug_file); +} +#else +static inline struct dentry *da9030_bat_create_debugfs(struct da9030_charger *charger) +{ + return NULL; +} +static inline void da9030_bat_remove_debugfs(struct da9030_charger *charger) +{ +} +#endif + +static inline void da9030_read_adc(struct da9030_charger *charger, + struct da9030_adc_res *adc) +{ + da903x_reads(charger->master, DA9030_VBAT_RES, + sizeof(*adc), (uint8_t *)adc); +} + +static void da9030_charger_update_state(struct da9030_charger *charger) +{ + uint8_t val; + + da903x_read(charger->master, DA9030_CHARGE_CONTROL, &val); + charger->is_on = (val & DA9030_CHRG_CHARGER_ENABLE) ? 1 : 0; + charger->mA = ((val >> 3) & 0xf) * 100; + charger->mV = (val & 0x7) * 50 + 4000; + + da9030_read_adc(charger, &charger->adc); + da903x_read(charger->master, DA9030_FAULT_LOG, &charger->fault); + charger->chdet = da903x_query_status(charger->master, + DA9030_STATUS_CHDET); +} + +static void da9030_set_charge(struct da9030_charger *charger, int on) +{ + uint8_t val; + + if (on) { + val = DA9030_CHRG_CHARGER_ENABLE; + val |= (charger->charge_milliamp / 100) << 3; + val |= (charger->charge_millivolt - 4000) / 50; + charger->is_on = 1; + } else { + val = 0; + charger->is_on = 0; + } + + da903x_write(charger->master, DA9030_CHARGE_CONTROL, val); + + power_supply_changed(charger->psy); +} + +static void da9030_charger_check_state(struct da9030_charger *charger) +{ + da9030_charger_update_state(charger); + + /* we wake or boot with external power on */ + if (!charger->is_on) { + if ((charger->chdet) && + (charger->adc.vbat_res < + charger->thresholds.vbat_charge_start)) { + da9030_set_charge(charger, 1); + } + } else { + /* Charger has been pulled out */ + if (!charger->chdet) { + da9030_set_charge(charger, 0); + return; + } + + if (charger->adc.vbat_res >= + charger->thresholds.vbat_charge_stop) { + da9030_set_charge(charger, 0); + da903x_write(charger->master, DA9030_VBATMON, + charger->thresholds.vbat_charge_restart); + } else if (charger->adc.vbat_res > + charger->thresholds.vbat_low) { + /* we are charging and passed LOW_THRESH, + so upate DA9030 VBAT threshold + */ + da903x_write(charger->master, DA9030_VBATMON, + charger->thresholds.vbat_low); + } + if (charger->adc.vchmax_res > charger->thresholds.vcharge_max || + charger->adc.vchmin_res < charger->thresholds.vcharge_min || + /* Tempreture readings are negative */ + charger->adc.tbat_res < charger->thresholds.tbat_high || + charger->adc.tbat_res > charger->thresholds.tbat_low) { + /* disable charger */ + da9030_set_charge(charger, 0); + } + } +} + +static void da9030_charging_monitor(struct work_struct *work) +{ + struct da9030_charger *charger; + + charger = container_of(work, struct da9030_charger, work.work); + + da9030_charger_check_state(charger); + + /* reschedule for the next time */ + schedule_delayed_work(&charger->work, charger->interval); +} + +static enum power_supply_property da9030_battery_props[] = { + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, +}; + +static void da9030_battery_check_status(struct da9030_charger *charger, + union power_supply_propval *val) +{ + if (charger->chdet) { + if (charger->is_on) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + } +} + +static void da9030_battery_check_health(struct da9030_charger *charger, + union power_supply_propval *val) +{ + if (charger->fault & DA9030_FAULT_LOG_OVER_TEMP) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (charger->fault & DA9030_FAULT_LOG_VBAT_OVER) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; +} + +static int da9030_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct da9030_charger *charger = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + da9030_battery_check_status(charger, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + da9030_battery_check_health(charger, val); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = charger->battery_info->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = charger->battery_info->voltage_max_design; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = charger->battery_info->voltage_min_design; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = da9030_reg_to_mV(charger->adc.vbat_res) * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + val->intval = + da9030_reg_to_mA(charger->adc.ichaverage_res) * 1000; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = charger->battery_info->name; + break; + default: + break; + } + + return 0; +} + +static void da9030_battery_vbat_event(struct da9030_charger *charger) +{ + da9030_read_adc(charger, &charger->adc); + + if (charger->is_on) + return; + + if (charger->adc.vbat_res < charger->thresholds.vbat_low) { + /* set VBAT threshold for critical */ + da903x_write(charger->master, DA9030_VBATMON, + charger->thresholds.vbat_crit); + if (charger->battery_low) + charger->battery_low(); + } else if (charger->adc.vbat_res < + charger->thresholds.vbat_crit) { + /* notify the system of battery critical */ + if (charger->battery_critical) + charger->battery_critical(); + } +} + +static int da9030_battery_event(struct notifier_block *nb, unsigned long event, + void *data) +{ + struct da9030_charger *charger = + container_of(nb, struct da9030_charger, nb); + + switch (event) { + case DA9030_EVENT_CHDET: + cancel_delayed_work_sync(&charger->work); + schedule_work(&charger->work.work); + break; + case DA9030_EVENT_VBATMON: + da9030_battery_vbat_event(charger); + break; + case DA9030_EVENT_CHIOVER: + case DA9030_EVENT_TBAT: + da9030_set_charge(charger, 0); + break; + } + + return 0; +} + +static void da9030_battery_convert_thresholds(struct da9030_charger *charger, + struct da9030_battery_info *pdata) +{ + charger->thresholds.tbat_low = pdata->tbat_low; + charger->thresholds.tbat_high = pdata->tbat_high; + charger->thresholds.tbat_restart = pdata->tbat_restart; + + charger->thresholds.vbat_low = + da9030_millivolt_to_reg(pdata->vbat_low); + charger->thresholds.vbat_crit = + da9030_millivolt_to_reg(pdata->vbat_crit); + charger->thresholds.vbat_charge_start = + da9030_millivolt_to_reg(pdata->vbat_charge_start); + charger->thresholds.vbat_charge_stop = + da9030_millivolt_to_reg(pdata->vbat_charge_stop); + charger->thresholds.vbat_charge_restart = + da9030_millivolt_to_reg(pdata->vbat_charge_restart); + + charger->thresholds.vcharge_min = + da9030_millivolt_to_reg(pdata->vcharge_min); + charger->thresholds.vcharge_max = + da9030_millivolt_to_reg(pdata->vcharge_max); +} + +static void da9030_battery_setup_psy(struct da9030_charger *charger) +{ + struct power_supply_desc *psy_desc = &charger->psy_desc; + struct power_supply_info *info = charger->battery_info; + + psy_desc->name = info->name; + psy_desc->use_for_apm = info->use_for_apm; + psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; + psy_desc->get_property = da9030_battery_get_property; + + psy_desc->properties = da9030_battery_props; + psy_desc->num_properties = ARRAY_SIZE(da9030_battery_props); +}; + +static int da9030_battery_charger_init(struct da9030_charger *charger) +{ + char v[5]; + int ret; + + v[0] = v[1] = charger->thresholds.vbat_low; + v[2] = charger->thresholds.tbat_high; + v[3] = charger->thresholds.tbat_restart; + v[4] = charger->thresholds.tbat_low; + + ret = da903x_writes(charger->master, DA9030_VBATMON, 5, v); + if (ret) + return ret; + + /* + * Enable reference voltage supply for ADC from the LDO_INTERNAL + * regulator. Must be set before ADC measurements can be made. + */ + ret = da903x_write(charger->master, DA9030_ADC_MAN_CONTROL, + DA9030_ADC_LDO_INT_ENABLE | + DA9030_ADC_TBATREF_ENABLE); + if (ret) + return ret; + + /* enable auto ADC measuremnts */ + return da903x_write(charger->master, DA9030_ADC_AUTO_CONTROL, + DA9030_ADC_TBAT_ENABLE | DA9030_ADC_VBAT_IN_TXON | + DA9030_ADC_VCH_ENABLE | DA9030_ADC_ICH_ENABLE | + DA9030_ADC_VBAT_ENABLE | + DA9030_ADC_AUTO_SLEEP_ENABLE); +} + +static int da9030_battery_probe(struct platform_device *pdev) +{ + struct da9030_charger *charger; + struct power_supply_config psy_cfg = {}; + struct da9030_battery_info *pdata = pdev->dev.platform_data; + int ret; + + if (pdata == NULL) + return -EINVAL; + + if (pdata->charge_milliamp >= 1500 || + pdata->charge_millivolt < 4000 || + pdata->charge_millivolt > 4350) + return -EINVAL; + + charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); + if (charger == NULL) + return -ENOMEM; + + charger->master = pdev->dev.parent; + + /* 10 seconds between monitor runs unless platform defines other + interval */ + charger->interval = msecs_to_jiffies( + (pdata->batmon_interval ? : 10) * 1000); + + charger->charge_milliamp = pdata->charge_milliamp; + charger->charge_millivolt = pdata->charge_millivolt; + charger->battery_info = pdata->battery_info; + charger->battery_low = pdata->battery_low; + charger->battery_critical = pdata->battery_critical; + + da9030_battery_convert_thresholds(charger, pdata); + + ret = da9030_battery_charger_init(charger); + if (ret) + goto err_charger_init; + + INIT_DELAYED_WORK(&charger->work, da9030_charging_monitor); + schedule_delayed_work(&charger->work, charger->interval); + + charger->nb.notifier_call = da9030_battery_event; + ret = da903x_register_notifier(charger->master, &charger->nb, + DA9030_EVENT_CHDET | + DA9030_EVENT_VBATMON | + DA9030_EVENT_CHIOVER | + DA9030_EVENT_TBAT); + if (ret) + goto err_notifier; + + da9030_battery_setup_psy(charger); + psy_cfg.drv_data = charger; + charger->psy = power_supply_register(&pdev->dev, &charger->psy_desc, + &psy_cfg); + if (IS_ERR(charger->psy)) { + ret = PTR_ERR(charger->psy); + goto err_ps_register; + } + + charger->debug_file = da9030_bat_create_debugfs(charger); + platform_set_drvdata(pdev, charger); + return 0; + +err_ps_register: + da903x_unregister_notifier(charger->master, &charger->nb, + DA9030_EVENT_CHDET | DA9030_EVENT_VBATMON | + DA9030_EVENT_CHIOVER | DA9030_EVENT_TBAT); +err_notifier: + cancel_delayed_work(&charger->work); + +err_charger_init: + return ret; +} + +static int da9030_battery_remove(struct platform_device *dev) +{ + struct da9030_charger *charger = platform_get_drvdata(dev); + + da9030_bat_remove_debugfs(charger); + + da903x_unregister_notifier(charger->master, &charger->nb, + DA9030_EVENT_CHDET | DA9030_EVENT_VBATMON | + DA9030_EVENT_CHIOVER | DA9030_EVENT_TBAT); + cancel_delayed_work_sync(&charger->work); + da9030_set_charge(charger, 0); + power_supply_unregister(charger->psy); + + return 0; +} + +static struct platform_driver da903x_battery_driver = { + .driver = { + .name = "da903x-battery", + }, + .probe = da9030_battery_probe, + .remove = da9030_battery_remove, +}; + +module_platform_driver(da903x_battery_driver); + +MODULE_DESCRIPTION("DA9030 battery charger driver"); +MODULE_AUTHOR("Mike Rapoport, CompuLab"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/da9052-battery.c b/drivers/power/supply/da9052-battery.c new file mode 100644 index 000000000..d87bdecc9 --- /dev/null +++ b/drivers/power/supply/da9052-battery.c @@ -0,0 +1,665 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Batttery Driver for Dialog DA9052 PMICs + * + * Copyright(c) 2011 Dialog Semiconductor Ltd. + * + * Author: David Dajun Chen <dchen@diasemi.com> + */ + +#include <linux/delay.h> +#include <linux/freezer.h> +#include <linux/fs.h> +#include <linux/jiffies.h> +#include <linux/module.h> +#include <linux/timer.h> +#include <linux/uaccess.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> + +#include <linux/mfd/da9052/da9052.h> +#include <linux/mfd/da9052/pdata.h> +#include <linux/mfd/da9052/reg.h> + +/* STATIC CONFIGURATION */ +#define DA9052_BAT_CUTOFF_VOLT 2800 +#define DA9052_BAT_TSH 62000 +#define DA9052_BAT_LOW_CAP 4 +#define DA9052_AVG_SZ 4 +#define DA9052_VC_TBL_SZ 68 +#define DA9052_VC_TBL_REF_SZ 3 + +#define DA9052_ISET_USB_MASK 0x0F +#define DA9052_CHG_USB_ILIM_MASK 0x40 +#define DA9052_CHG_LIM_COLS 16 + +#define DA9052_MEAN(x, y) ((x + y) / 2) + +enum charger_type_enum { + DA9052_NOCHARGER = 1, + DA9052_CHARGER, +}; + +static const u16 da9052_chg_current_lim[2][DA9052_CHG_LIM_COLS] = { + {70, 80, 90, 100, 110, 120, 400, 450, + 500, 550, 600, 650, 700, 900, 1100, 1300}, + {80, 90, 100, 110, 120, 400, 450, 500, + 550, 600, 800, 1000, 1200, 1400, 1600, 1800}, +}; + +static const u16 vc_tbl_ref[3] = {10, 25, 40}; +/* Lookup table for voltage vs capacity */ +static u32 const vc_tbl[3][68][2] = { + /* For temperature 10 degree Celsius */ + { + {4082, 100}, {4036, 98}, + {4020, 96}, {4008, 95}, + {3997, 93}, {3983, 91}, + {3964, 90}, {3943, 88}, + {3926, 87}, {3912, 85}, + {3900, 84}, {3890, 82}, + {3881, 80}, {3873, 79}, + {3865, 77}, {3857, 76}, + {3848, 74}, {3839, 73}, + {3829, 71}, {3820, 70}, + {3811, 68}, {3802, 67}, + {3794, 65}, {3785, 64}, + {3778, 62}, {3770, 61}, + {3763, 59}, {3756, 58}, + {3750, 56}, {3744, 55}, + {3738, 53}, {3732, 52}, + {3727, 50}, {3722, 49}, + {3717, 47}, {3712, 46}, + {3708, 44}, {3703, 43}, + {3700, 41}, {3696, 40}, + {3693, 38}, {3691, 37}, + {3688, 35}, {3686, 34}, + {3683, 32}, {3681, 31}, + {3678, 29}, {3675, 28}, + {3672, 26}, {3669, 25}, + {3665, 23}, {3661, 22}, + {3656, 21}, {3651, 19}, + {3645, 18}, {3639, 16}, + {3631, 15}, {3622, 13}, + {3611, 12}, {3600, 10}, + {3587, 9}, {3572, 7}, + {3548, 6}, {3503, 5}, + {3420, 3}, {3268, 2}, + {2992, 1}, {2746, 0} + }, + /* For temperature 25 degree Celsius */ + { + {4102, 100}, {4065, 98}, + {4048, 96}, {4034, 95}, + {4021, 93}, {4011, 92}, + {4001, 90}, {3986, 88}, + {3968, 87}, {3952, 85}, + {3938, 84}, {3926, 82}, + {3916, 81}, {3908, 79}, + {3900, 77}, {3892, 76}, + {3883, 74}, {3874, 73}, + {3864, 71}, {3855, 70}, + {3846, 68}, {3836, 67}, + {3827, 65}, {3819, 64}, + {3810, 62}, {3801, 61}, + {3793, 59}, {3786, 58}, + {3778, 56}, {3772, 55}, + {3765, 53}, {3759, 52}, + {3754, 50}, {3748, 49}, + {3743, 47}, {3738, 46}, + {3733, 44}, {3728, 43}, + {3724, 41}, {3720, 40}, + {3716, 38}, {3712, 37}, + {3709, 35}, {3706, 34}, + {3703, 33}, {3701, 31}, + {3698, 30}, {3696, 28}, + {3693, 27}, {3690, 25}, + {3687, 24}, {3683, 22}, + {3680, 21}, {3675, 19}, + {3671, 18}, {3666, 17}, + {3660, 15}, {3654, 14}, + {3647, 12}, {3639, 11}, + {3630, 9}, {3621, 8}, + {3613, 6}, {3606, 5}, + {3597, 4}, {3582, 2}, + {3546, 1}, {2747, 0} + }, + /* For temperature 40 degree Celsius */ + { + {4114, 100}, {4081, 98}, + {4065, 96}, {4050, 95}, + {4036, 93}, {4024, 92}, + {4013, 90}, {4002, 88}, + {3990, 87}, {3976, 85}, + {3962, 84}, {3950, 82}, + {3939, 81}, {3930, 79}, + {3921, 77}, {3912, 76}, + {3902, 74}, {3893, 73}, + {3883, 71}, {3874, 70}, + {3865, 68}, {3856, 67}, + {3847, 65}, {3838, 64}, + {3829, 62}, {3820, 61}, + {3812, 59}, {3803, 58}, + {3795, 56}, {3787, 55}, + {3780, 53}, {3773, 52}, + {3767, 50}, {3761, 49}, + {3756, 47}, {3751, 46}, + {3746, 44}, {3741, 43}, + {3736, 41}, {3732, 40}, + {3728, 38}, {3724, 37}, + {3720, 35}, {3716, 34}, + {3713, 33}, {3710, 31}, + {3707, 30}, {3704, 28}, + {3701, 27}, {3698, 25}, + {3695, 24}, {3691, 22}, + {3686, 21}, {3681, 19}, + {3676, 18}, {3671, 17}, + {3666, 15}, {3661, 14}, + {3655, 12}, {3648, 11}, + {3640, 9}, {3632, 8}, + {3622, 6}, {3616, 5}, + {3611, 4}, {3604, 2}, + {3594, 1}, {2747, 0} + } +}; + +struct da9052_battery { + struct da9052 *da9052; + struct power_supply *psy; + struct notifier_block nb; + int charger_type; + int status; + int health; +}; + +static inline int volt_reg_to_mV(int value) +{ + return ((value * 1000) / 512) + 2500; +} + +static inline int ichg_reg_to_mA(int value) +{ + return (value * 3900) / 1000; +} + +static int da9052_read_chgend_current(struct da9052_battery *bat, + int *current_mA) +{ + int ret; + + if (bat->status == POWER_SUPPLY_STATUS_DISCHARGING) + return -EINVAL; + + ret = da9052_reg_read(bat->da9052, DA9052_ICHG_END_REG); + if (ret < 0) + return ret; + + *current_mA = ichg_reg_to_mA(ret & DA9052_ICHGEND_ICHGEND); + + return 0; +} + +static int da9052_read_chg_current(struct da9052_battery *bat, int *current_mA) +{ + int ret; + + if (bat->status == POWER_SUPPLY_STATUS_DISCHARGING) + return -EINVAL; + + ret = da9052_reg_read(bat->da9052, DA9052_ICHG_AV_REG); + if (ret < 0) + return ret; + + *current_mA = ichg_reg_to_mA(ret & DA9052_ICHGAV_ICHGAV); + + return 0; +} + +static int da9052_bat_check_status(struct da9052_battery *bat, int *status) +{ + u8 v[2] = {0, 0}; + u8 bat_status; + u8 chg_end; + int ret; + int chg_current; + int chg_end_current; + bool dcinsel; + bool dcindet; + bool vbussel; + bool vbusdet; + bool dc; + bool vbus; + + ret = da9052_group_read(bat->da9052, DA9052_STATUS_A_REG, 2, v); + if (ret < 0) + return ret; + + bat_status = v[0]; + chg_end = v[1]; + + dcinsel = bat_status & DA9052_STATUSA_DCINSEL; + dcindet = bat_status & DA9052_STATUSA_DCINDET; + vbussel = bat_status & DA9052_STATUSA_VBUSSEL; + vbusdet = bat_status & DA9052_STATUSA_VBUSDET; + dc = dcinsel && dcindet; + vbus = vbussel && vbusdet; + + /* Preference to WALL(DCIN) charger unit */ + if (dc || vbus) { + bat->charger_type = DA9052_CHARGER; + + /* If charging end flag is set and Charging current is greater + * than charging end limit then battery is charging + */ + if ((chg_end & DA9052_STATUSB_CHGEND) != 0) { + ret = da9052_read_chg_current(bat, &chg_current); + if (ret < 0) + return ret; + ret = da9052_read_chgend_current(bat, &chg_end_current); + if (ret < 0) + return ret; + + if (chg_current >= chg_end_current) + bat->status = POWER_SUPPLY_STATUS_CHARGING; + else + bat->status = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + /* If Charging end flag is cleared then battery is + * charging + */ + bat->status = POWER_SUPPLY_STATUS_CHARGING; + } + } else if (dcindet || vbusdet) { + bat->charger_type = DA9052_CHARGER; + bat->status = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + bat->charger_type = DA9052_NOCHARGER; + bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + } + + if (status != NULL) + *status = bat->status; + return 0; +} + +static int da9052_bat_read_volt(struct da9052_battery *bat, int *volt_mV) +{ + int volt; + + volt = da9052_adc_manual_read(bat->da9052, DA9052_ADC_MAN_MUXSEL_VBAT); + if (volt < 0) + return volt; + + *volt_mV = volt_reg_to_mV(volt); + + return 0; +} + +static int da9052_bat_check_presence(struct da9052_battery *bat, int *illegal) +{ + int bat_temp; + + bat_temp = da9052_adc_read_temp(bat->da9052); + if (bat_temp < 0) + return bat_temp; + + if (bat_temp > DA9052_BAT_TSH) + *illegal = 1; + else + *illegal = 0; + + return 0; +} + +static int da9052_bat_interpolate(int vbat_lower, int vbat_upper, + int level_lower, int level_upper, + int bat_voltage) +{ + int tmp; + + tmp = ((level_upper - level_lower) * 1000) / (vbat_upper - vbat_lower); + tmp = level_lower + (((bat_voltage - vbat_lower) * tmp) / 1000); + + return tmp; +} + +static unsigned char da9052_determine_vc_tbl_index(unsigned char adc_temp) +{ + int i; + + if (adc_temp <= vc_tbl_ref[0]) + return 0; + + if (adc_temp > vc_tbl_ref[DA9052_VC_TBL_REF_SZ - 1]) + return DA9052_VC_TBL_REF_SZ - 1; + + for (i = 0; i < DA9052_VC_TBL_REF_SZ - 1; i++) { + if ((adc_temp > vc_tbl_ref[i]) && + (adc_temp <= DA9052_MEAN(vc_tbl_ref[i], vc_tbl_ref[i + 1]))) + return i; + if ((adc_temp > DA9052_MEAN(vc_tbl_ref[i], vc_tbl_ref[i + 1])) + && (adc_temp <= vc_tbl_ref[i])) + return i + 1; + } + /* + * For some reason authors of the driver didn't presume that we can + * end up here. It might be OK, but might be not, no one knows for + * sure. Go check your battery, is it on fire? + */ + WARN_ON(1); + return 0; +} + +static int da9052_bat_read_capacity(struct da9052_battery *bat, int *capacity) +{ + int adc_temp; + int bat_voltage; + int vbat_lower; + int vbat_upper; + int level_upper; + int level_lower; + int ret; + int flag; + int i = 0; + int j; + + ret = da9052_bat_read_volt(bat, &bat_voltage); + if (ret < 0) + return ret; + + adc_temp = da9052_adc_read_temp(bat->da9052); + if (adc_temp < 0) + return adc_temp; + + i = da9052_determine_vc_tbl_index(adc_temp); + + if (bat_voltage >= vc_tbl[i][0][0]) { + *capacity = 100; + return 0; + } + if (bat_voltage <= vc_tbl[i][DA9052_VC_TBL_SZ - 1][0]) { + *capacity = 0; + return 0; + } + flag = 0; + + for (j = 0; j < (DA9052_VC_TBL_SZ-1); j++) { + if ((bat_voltage <= vc_tbl[i][j][0]) && + (bat_voltage >= vc_tbl[i][j + 1][0])) { + vbat_upper = vc_tbl[i][j][0]; + vbat_lower = vc_tbl[i][j + 1][0]; + level_upper = vc_tbl[i][j][1]; + level_lower = vc_tbl[i][j + 1][1]; + flag = 1; + break; + } + } + if (!flag) + return -EIO; + + *capacity = da9052_bat_interpolate(vbat_lower, vbat_upper, level_lower, + level_upper, bat_voltage); + + return 0; +} + +static int da9052_bat_check_health(struct da9052_battery *bat, int *health) +{ + int ret; + int bat_illegal; + int capacity; + + ret = da9052_bat_check_presence(bat, &bat_illegal); + if (ret < 0) + return ret; + + if (bat_illegal) { + bat->health = POWER_SUPPLY_HEALTH_UNKNOWN; + return 0; + } + + if (bat->health != POWER_SUPPLY_HEALTH_OVERHEAT) { + ret = da9052_bat_read_capacity(bat, &capacity); + if (ret < 0) + return ret; + if (capacity < DA9052_BAT_LOW_CAP) + bat->health = POWER_SUPPLY_HEALTH_DEAD; + else + bat->health = POWER_SUPPLY_HEALTH_GOOD; + } + + *health = bat->health; + + return 0; +} + +static irqreturn_t da9052_bat_irq(int irq, void *data) +{ + struct da9052_battery *bat = data; + int virq; + + virq = regmap_irq_get_virq(bat->da9052->irq_data, irq); + irq -= virq; + + if (irq == DA9052_IRQ_CHGEND) + bat->status = POWER_SUPPLY_STATUS_FULL; + else + da9052_bat_check_status(bat, NULL); + + if (irq == DA9052_IRQ_CHGEND || irq == DA9052_IRQ_DCIN || + irq == DA9052_IRQ_VBUS || irq == DA9052_IRQ_TBAT) { + power_supply_changed(bat->psy); + } + + return IRQ_HANDLED; +} + +static int da9052_USB_current_notifier(struct notifier_block *nb, + unsigned long events, void *data) +{ + u8 row; + u8 col; + int *current_mA = data; + int ret; + struct da9052_battery *bat = container_of(nb, struct da9052_battery, + nb); + + if (bat->status == POWER_SUPPLY_STATUS_DISCHARGING) + return -EPERM; + + ret = da9052_reg_read(bat->da9052, DA9052_CHGBUCK_REG); + if (ret & DA9052_CHG_USB_ILIM_MASK) + return -EPERM; + + if (bat->da9052->chip_id == DA9052) + row = 0; + else + row = 1; + + if (*current_mA < da9052_chg_current_lim[row][0] || + *current_mA > da9052_chg_current_lim[row][DA9052_CHG_LIM_COLS - 1]) + return -EINVAL; + + for (col = 0; col <= DA9052_CHG_LIM_COLS - 1 ; col++) { + if (*current_mA <= da9052_chg_current_lim[row][col]) + break; + } + + return da9052_reg_update(bat->da9052, DA9052_ISET_REG, + DA9052_ISET_USB_MASK, col); +} + +static int da9052_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret; + int illegal; + struct da9052_battery *bat = power_supply_get_drvdata(psy); + + ret = da9052_bat_check_presence(bat, &illegal); + if (ret < 0) + return ret; + + if (illegal && psp != POWER_SUPPLY_PROP_PRESENT) + return -ENODEV; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = da9052_bat_check_status(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = + (bat->charger_type == DA9052_NOCHARGER) ? 0 : 1; + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = da9052_bat_check_presence(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = da9052_bat_check_health(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = DA9052_BAT_CUTOFF_VOLT * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + ret = da9052_bat_read_volt(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = da9052_read_chg_current(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = da9052_bat_read_capacity(bat, &val->intval); + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = da9052_adc_read_temp(bat->da9052); + ret = val->intval; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + default: + return -EINVAL; + } + return ret; +} + +static enum power_supply_property da9052_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, +}; + +static struct power_supply_desc psy_desc = { + .name = "da9052-bat", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = da9052_bat_props, + .num_properties = ARRAY_SIZE(da9052_bat_props), + .get_property = da9052_bat_get_property, +}; + +static char *da9052_bat_irqs[] = { + "BATT TEMP", + "DCIN DET", + "DCIN REM", + "VBUS DET", + "VBUS REM", + "CHG END", +}; + +static int da9052_bat_irq_bits[] = { + DA9052_IRQ_TBAT, + DA9052_IRQ_DCIN, + DA9052_IRQ_DCINREM, + DA9052_IRQ_VBUS, + DA9052_IRQ_VBUSREM, + DA9052_IRQ_CHGEND, +}; + +static s32 da9052_bat_probe(struct platform_device *pdev) +{ + struct da9052_pdata *pdata; + struct da9052_battery *bat; + struct power_supply_config psy_cfg = {}; + int ret; + int i; + + bat = devm_kzalloc(&pdev->dev, sizeof(struct da9052_battery), + GFP_KERNEL); + if (!bat) + return -ENOMEM; + + psy_cfg.drv_data = bat; + + bat->da9052 = dev_get_drvdata(pdev->dev.parent); + bat->charger_type = DA9052_NOCHARGER; + bat->status = POWER_SUPPLY_STATUS_UNKNOWN; + bat->health = POWER_SUPPLY_HEALTH_UNKNOWN; + bat->nb.notifier_call = da9052_USB_current_notifier; + + pdata = bat->da9052->dev->platform_data; + if (pdata != NULL && pdata->use_for_apm) + psy_desc.use_for_apm = pdata->use_for_apm; + else + psy_desc.use_for_apm = 1; + + for (i = 0; i < ARRAY_SIZE(da9052_bat_irqs); i++) { + ret = da9052_request_irq(bat->da9052, + da9052_bat_irq_bits[i], da9052_bat_irqs[i], + da9052_bat_irq, bat); + + if (ret != 0) { + dev_err(bat->da9052->dev, + "DA9052 failed to request %s IRQ: %d\n", + da9052_bat_irqs[i], ret); + goto err; + } + } + + bat->psy = power_supply_register(&pdev->dev, &psy_desc, &psy_cfg); + if (IS_ERR(bat->psy)) { + ret = PTR_ERR(bat->psy); + goto err; + } + + platform_set_drvdata(pdev, bat); + return 0; + +err: + while (--i >= 0) + da9052_free_irq(bat->da9052, da9052_bat_irq_bits[i], bat); + + return ret; +} +static int da9052_bat_remove(struct platform_device *pdev) +{ + int i; + struct da9052_battery *bat = platform_get_drvdata(pdev); + + for (i = 0; i < ARRAY_SIZE(da9052_bat_irqs); i++) + da9052_free_irq(bat->da9052, da9052_bat_irq_bits[i], bat); + + power_supply_unregister(bat->psy); + + return 0; +} + +static struct platform_driver da9052_bat_driver = { + .probe = da9052_bat_probe, + .remove = da9052_bat_remove, + .driver = { + .name = "da9052-bat", + }, +}; +module_platform_driver(da9052_bat_driver); + +MODULE_DESCRIPTION("DA9052 BAT Device Driver"); +MODULE_AUTHOR("David Dajun Chen <dchen@diasemi.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:da9052-bat"); diff --git a/drivers/power/supply/da9150-charger.c b/drivers/power/supply/da9150-charger.c new file mode 100644 index 000000000..6b987da58 --- /dev/null +++ b/drivers/power/supply/da9150-charger.c @@ -0,0 +1,691 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DA9150 Charger Driver + * + * Copyright (c) 2014 Dialog Semiconductor + * + * Author: Adam Thomson <Adam.Thomson.Opensource@diasemi.com> + */ + +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/interrupt.h> +#include <linux/power_supply.h> +#include <linux/notifier.h> +#include <linux/usb/phy.h> +#include <linux/iio/consumer.h> +#include <linux/mfd/da9150/core.h> +#include <linux/mfd/da9150/registers.h> + +/* Private data */ +struct da9150_charger { + struct da9150 *da9150; + struct device *dev; + + struct power_supply *usb; + struct power_supply *battery; + struct power_supply *supply_online; + + struct usb_phy *usb_phy; + struct notifier_block otg_nb; + struct work_struct otg_work; + unsigned long usb_event; + + struct iio_channel *ibus_chan; + struct iio_channel *vbus_chan; + struct iio_channel *tjunc_chan; + struct iio_channel *vbat_chan; +}; + +static inline int da9150_charger_supply_online(struct da9150_charger *charger, + struct power_supply *psy, + union power_supply_propval *val) +{ + val->intval = (psy == charger->supply_online) ? 1 : 0; + + return 0; +} + +/* Charger Properties */ +static int da9150_charger_vbus_voltage_now(struct da9150_charger *charger, + union power_supply_propval *val) +{ + int v_val, ret; + + /* Read processed value - mV units */ + ret = iio_read_channel_processed(charger->vbus_chan, &v_val); + if (ret < 0) + return ret; + + /* Convert voltage to expected uV units */ + val->intval = v_val * 1000; + + return 0; +} + +static int da9150_charger_ibus_current_avg(struct da9150_charger *charger, + union power_supply_propval *val) +{ + int i_val, ret; + + /* Read processed value - mA units */ + ret = iio_read_channel_processed(charger->ibus_chan, &i_val); + if (ret < 0) + return ret; + + /* Convert current to expected uA units */ + val->intval = i_val * 1000; + + return 0; +} + +static int da9150_charger_tjunc_temp(struct da9150_charger *charger, + union power_supply_propval *val) +{ + int t_val, ret; + + /* Read processed value - 0.001 degrees C units */ + ret = iio_read_channel_processed(charger->tjunc_chan, &t_val); + if (ret < 0) + return ret; + + /* Convert temp to expect 0.1 degrees C units */ + val->intval = t_val / 100; + + return 0; +} + +static enum power_supply_property da9150_charger_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_TEMP, +}; + +static int da9150_charger_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct da9150_charger *charger = dev_get_drvdata(psy->dev.parent); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = da9150_charger_supply_online(charger, psy, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = da9150_charger_vbus_voltage_now(charger, val); + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = da9150_charger_ibus_current_avg(charger, val); + break; + case POWER_SUPPLY_PROP_TEMP: + ret = da9150_charger_tjunc_temp(charger, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +/* Battery Properties */ +static int da9150_charger_battery_status(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + /* Check to see if battery is discharging */ + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_H); + + if (((reg & DA9150_VBUS_STAT_MASK) == DA9150_VBUS_STAT_OFF) || + ((reg & DA9150_VBUS_STAT_MASK) == DA9150_VBUS_STAT_WAIT)) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + + return 0; + } + + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_J); + + /* Now check for other states */ + switch (reg & DA9150_CHG_STAT_MASK) { + case DA9150_CHG_STAT_ACT: + case DA9150_CHG_STAT_PRE: + case DA9150_CHG_STAT_CC: + case DA9150_CHG_STAT_CV: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case DA9150_CHG_STAT_OFF: + case DA9150_CHG_STAT_SUSP: + case DA9150_CHG_STAT_TEMP: + case DA9150_CHG_STAT_TIME: + case DA9150_CHG_STAT_BAT: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case DA9150_CHG_STAT_FULL: + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + + return 0; +} + +static int da9150_charger_battery_health(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_J); + + /* Check if temperature limit reached */ + switch (reg & DA9150_CHG_TEMP_MASK) { + case DA9150_CHG_TEMP_UNDER: + val->intval = POWER_SUPPLY_HEALTH_COLD; + return 0; + case DA9150_CHG_TEMP_OVER: + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + return 0; + default: + break; + } + + /* Check for other health states */ + switch (reg & DA9150_CHG_STAT_MASK) { + case DA9150_CHG_STAT_ACT: + case DA9150_CHG_STAT_PRE: + val->intval = POWER_SUPPLY_HEALTH_DEAD; + break; + case DA9150_CHG_STAT_TIME: + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + default: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + } + + return 0; +} + +static int da9150_charger_battery_present(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + /* Check if battery present or removed */ + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_J); + if ((reg & DA9150_CHG_STAT_MASK) == DA9150_CHG_STAT_BAT) + val->intval = 0; + else + val->intval = 1; + + return 0; +} + +static int da9150_charger_battery_charge_type(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_J); + + switch (reg & DA9150_CHG_STAT_MASK) { + case DA9150_CHG_STAT_CC: + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case DA9150_CHG_STAT_ACT: + case DA9150_CHG_STAT_PRE: + case DA9150_CHG_STAT_CV: + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + default: + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + } + + return 0; +} + +static int da9150_charger_battery_voltage_min(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + reg = da9150_reg_read(charger->da9150, DA9150_PPR_CHGCTRL_C); + + /* Value starts at 2500 mV, 50 mV increments, presented in uV */ + val->intval = ((reg & DA9150_CHG_VFAULT_MASK) * 50000) + 2500000; + + return 0; +} + +static int da9150_charger_battery_voltage_now(struct da9150_charger *charger, + union power_supply_propval *val) +{ + int v_val, ret; + + /* Read processed value - mV units */ + ret = iio_read_channel_processed(charger->vbat_chan, &v_val); + if (ret < 0) + return ret; + + val->intval = v_val * 1000; + + return 0; +} + +static int da9150_charger_battery_current_max(struct da9150_charger *charger, + union power_supply_propval *val) +{ + int reg; + + reg = da9150_reg_read(charger->da9150, DA9150_PPR_CHGCTRL_D); + + /* 25mA increments */ + val->intval = reg * 25000; + + return 0; +} + +static int da9150_charger_battery_voltage_max(struct da9150_charger *charger, + union power_supply_propval *val) +{ + u8 reg; + + reg = da9150_reg_read(charger->da9150, DA9150_PPR_CHGCTRL_B); + + /* Value starts at 3650 mV, 25 mV increments, presented in uV */ + val->intval = ((reg & DA9150_CHG_VBAT_MASK) * 25000) + 3650000; + return 0; +} + +static enum power_supply_property da9150_charger_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, +}; + +static int da9150_charger_battery_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct da9150_charger *charger = dev_get_drvdata(psy->dev.parent); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = da9150_charger_battery_status(charger, val); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = da9150_charger_supply_online(charger, psy, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = da9150_charger_battery_health(charger, val); + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = da9150_charger_battery_present(charger, val); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = da9150_charger_battery_charge_type(charger, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + ret = da9150_charger_battery_voltage_min(charger, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = da9150_charger_battery_voltage_now(charger, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + ret = da9150_charger_battery_current_max(charger, val); + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + ret = da9150_charger_battery_voltage_max(charger, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static irqreturn_t da9150_charger_chg_irq(int irq, void *data) +{ + struct da9150_charger *charger = data; + + power_supply_changed(charger->battery); + + return IRQ_HANDLED; +} + +static irqreturn_t da9150_charger_tjunc_irq(int irq, void *data) +{ + struct da9150_charger *charger = data; + + /* Nothing we can really do except report this. */ + dev_crit(charger->dev, "TJunc over temperature!!!\n"); + power_supply_changed(charger->usb); + + return IRQ_HANDLED; +} + +static irqreturn_t da9150_charger_vfault_irq(int irq, void *data) +{ + struct da9150_charger *charger = data; + + /* Nothing we can really do except report this. */ + dev_crit(charger->dev, "VSYS under voltage!!!\n"); + power_supply_changed(charger->usb); + power_supply_changed(charger->battery); + + return IRQ_HANDLED; +} + +static irqreturn_t da9150_charger_vbus_irq(int irq, void *data) +{ + struct da9150_charger *charger = data; + u8 reg; + + reg = da9150_reg_read(charger->da9150, DA9150_STATUS_H); + + /* Charger plugged in or battery only */ + switch (reg & DA9150_VBUS_STAT_MASK) { + case DA9150_VBUS_STAT_OFF: + case DA9150_VBUS_STAT_WAIT: + charger->supply_online = charger->battery; + break; + case DA9150_VBUS_STAT_CHG: + charger->supply_online = charger->usb; + break; + default: + dev_warn(charger->dev, "Unknown VBUS state - reg = 0x%x\n", + reg); + charger->supply_online = NULL; + break; + } + + power_supply_changed(charger->usb); + power_supply_changed(charger->battery); + + return IRQ_HANDLED; +} + +static void da9150_charger_otg_work(struct work_struct *data) +{ + struct da9150_charger *charger = + container_of(data, struct da9150_charger, otg_work); + + switch (charger->usb_event) { + case USB_EVENT_ID: + /* Enable OTG Boost */ + da9150_set_bits(charger->da9150, DA9150_PPR_BKCTRL_A, + DA9150_VBUS_MODE_MASK, DA9150_VBUS_MODE_OTG); + break; + case USB_EVENT_NONE: + /* Revert to charge mode */ + power_supply_changed(charger->usb); + power_supply_changed(charger->battery); + da9150_set_bits(charger->da9150, DA9150_PPR_BKCTRL_A, + DA9150_VBUS_MODE_MASK, DA9150_VBUS_MODE_CHG); + break; + } +} + +static int da9150_charger_otg_ncb(struct notifier_block *nb, unsigned long val, + void *priv) +{ + struct da9150_charger *charger = + container_of(nb, struct da9150_charger, otg_nb); + + dev_dbg(charger->dev, "DA9150 OTG notify %lu\n", val); + + charger->usb_event = val; + schedule_work(&charger->otg_work); + + return NOTIFY_OK; +} + +static int da9150_charger_register_irq(struct platform_device *pdev, + irq_handler_t handler, + const char *irq_name) +{ + struct device *dev = &pdev->dev; + struct da9150_charger *charger = platform_get_drvdata(pdev); + int irq, ret; + + irq = platform_get_irq_byname(pdev, irq_name); + if (irq < 0) { + dev_err(dev, "Failed to get IRQ CHG_STATUS: %d\n", irq); + return irq; + } + + ret = request_threaded_irq(irq, NULL, handler, IRQF_ONESHOT, irq_name, + charger); + if (ret) + dev_err(dev, "Failed to request IRQ %d: %d\n", irq, ret); + + return ret; +} + +static void da9150_charger_unregister_irq(struct platform_device *pdev, + const char *irq_name) +{ + struct device *dev = &pdev->dev; + struct da9150_charger *charger = platform_get_drvdata(pdev); + int irq; + + irq = platform_get_irq_byname(pdev, irq_name); + if (irq < 0) { + dev_err(dev, "Failed to get IRQ CHG_STATUS: %d\n", irq); + return; + } + + free_irq(irq, charger); +} + +static const struct power_supply_desc usb_desc = { + .name = "da9150-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = da9150_charger_props, + .num_properties = ARRAY_SIZE(da9150_charger_props), + .get_property = da9150_charger_get_prop, +}; + +static const struct power_supply_desc battery_desc = { + .name = "da9150-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = da9150_charger_bat_props, + .num_properties = ARRAY_SIZE(da9150_charger_bat_props), + .get_property = da9150_charger_battery_get_prop, +}; + +static int da9150_charger_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct da9150 *da9150 = dev_get_drvdata(dev->parent); + struct da9150_charger *charger; + u8 reg; + int ret; + + charger = devm_kzalloc(dev, sizeof(struct da9150_charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + platform_set_drvdata(pdev, charger); + charger->da9150 = da9150; + charger->dev = dev; + + /* Acquire ADC channels */ + charger->ibus_chan = iio_channel_get(dev, "CHAN_IBUS"); + if (IS_ERR(charger->ibus_chan)) { + ret = PTR_ERR(charger->ibus_chan); + goto ibus_chan_fail; + } + + charger->vbus_chan = iio_channel_get(dev, "CHAN_VBUS"); + if (IS_ERR(charger->vbus_chan)) { + ret = PTR_ERR(charger->vbus_chan); + goto vbus_chan_fail; + } + + charger->tjunc_chan = iio_channel_get(dev, "CHAN_TJUNC"); + if (IS_ERR(charger->tjunc_chan)) { + ret = PTR_ERR(charger->tjunc_chan); + goto tjunc_chan_fail; + } + + charger->vbat_chan = iio_channel_get(dev, "CHAN_VBAT"); + if (IS_ERR(charger->vbat_chan)) { + ret = PTR_ERR(charger->vbat_chan); + goto vbat_chan_fail; + } + + /* Register power supplies */ + charger->usb = power_supply_register(dev, &usb_desc, NULL); + if (IS_ERR(charger->usb)) { + ret = PTR_ERR(charger->usb); + goto usb_fail; + } + + charger->battery = power_supply_register(dev, &battery_desc, NULL); + if (IS_ERR(charger->battery)) { + ret = PTR_ERR(charger->battery); + goto battery_fail; + } + + /* Get initial online supply */ + reg = da9150_reg_read(da9150, DA9150_STATUS_H); + + switch (reg & DA9150_VBUS_STAT_MASK) { + case DA9150_VBUS_STAT_OFF: + case DA9150_VBUS_STAT_WAIT: + charger->supply_online = charger->battery; + break; + case DA9150_VBUS_STAT_CHG: + charger->supply_online = charger->usb; + break; + default: + dev_warn(dev, "Unknown VBUS state - reg = 0x%x\n", reg); + charger->supply_online = NULL; + break; + } + + /* Setup OTG reporting & configuration */ + charger->usb_phy = devm_usb_get_phy(dev, USB_PHY_TYPE_USB2); + if (!IS_ERR_OR_NULL(charger->usb_phy)) { + INIT_WORK(&charger->otg_work, da9150_charger_otg_work); + charger->otg_nb.notifier_call = da9150_charger_otg_ncb; + usb_register_notifier(charger->usb_phy, &charger->otg_nb); + } + + /* Register IRQs */ + ret = da9150_charger_register_irq(pdev, da9150_charger_chg_irq, + "CHG_STATUS"); + if (ret < 0) + goto chg_irq_fail; + + ret = da9150_charger_register_irq(pdev, da9150_charger_tjunc_irq, + "CHG_TJUNC"); + if (ret < 0) + goto tjunc_irq_fail; + + ret = da9150_charger_register_irq(pdev, da9150_charger_vfault_irq, + "CHG_VFAULT"); + if (ret < 0) + goto vfault_irq_fail; + + ret = da9150_charger_register_irq(pdev, da9150_charger_vbus_irq, + "CHG_VBUS"); + if (ret < 0) + goto vbus_irq_fail; + + return 0; + + +vbus_irq_fail: + da9150_charger_unregister_irq(pdev, "CHG_VFAULT"); +vfault_irq_fail: + da9150_charger_unregister_irq(pdev, "CHG_TJUNC"); +tjunc_irq_fail: + da9150_charger_unregister_irq(pdev, "CHG_STATUS"); +chg_irq_fail: + if (!IS_ERR_OR_NULL(charger->usb_phy)) + usb_unregister_notifier(charger->usb_phy, &charger->otg_nb); +battery_fail: + power_supply_unregister(charger->usb); + +usb_fail: + iio_channel_release(charger->vbat_chan); + +vbat_chan_fail: + iio_channel_release(charger->tjunc_chan); + +tjunc_chan_fail: + iio_channel_release(charger->vbus_chan); + +vbus_chan_fail: + iio_channel_release(charger->ibus_chan); + +ibus_chan_fail: + return ret; +} + +static int da9150_charger_remove(struct platform_device *pdev) +{ + struct da9150_charger *charger = platform_get_drvdata(pdev); + int irq; + + /* Make sure IRQs are released before unregistering power supplies */ + irq = platform_get_irq_byname(pdev, "CHG_VBUS"); + free_irq(irq, charger); + + irq = platform_get_irq_byname(pdev, "CHG_VFAULT"); + free_irq(irq, charger); + + irq = platform_get_irq_byname(pdev, "CHG_TJUNC"); + free_irq(irq, charger); + + irq = platform_get_irq_byname(pdev, "CHG_STATUS"); + free_irq(irq, charger); + + if (!IS_ERR_OR_NULL(charger->usb_phy)) + usb_unregister_notifier(charger->usb_phy, &charger->otg_nb); + cancel_work_sync(&charger->otg_work); + + power_supply_unregister(charger->battery); + power_supply_unregister(charger->usb); + + /* Release ADC channels */ + iio_channel_release(charger->ibus_chan); + iio_channel_release(charger->vbus_chan); + iio_channel_release(charger->tjunc_chan); + iio_channel_release(charger->vbat_chan); + + return 0; +} + +static struct platform_driver da9150_charger_driver = { + .driver = { + .name = "da9150-charger", + }, + .probe = da9150_charger_probe, + .remove = da9150_charger_remove, +}; + +module_platform_driver(da9150_charger_driver); + +MODULE_DESCRIPTION("Charger Driver for DA9150"); +MODULE_AUTHOR("Adam Thomson <Adam.Thomson.Opensource@diasemi.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/da9150-fg.c b/drivers/power/supply/da9150-fg.c new file mode 100644 index 000000000..6e367826a --- /dev/null +++ b/drivers/power/supply/da9150-fg.c @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * DA9150 Fuel-Gauge Driver + * + * Copyright (c) 2015 Dialog Semiconductor + * + * Author: Adam Thomson <Adam.Thomson.Opensource@diasemi.com> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/power_supply.h> +#include <linux/list.h> +#include <asm/div64.h> +#include <linux/mfd/da9150/core.h> +#include <linux/mfd/da9150/registers.h> + +/* Core2Wire */ +#define DA9150_QIF_READ (0x0 << 7) +#define DA9150_QIF_WRITE (0x1 << 7) +#define DA9150_QIF_CODE_MASK 0x7F + +#define DA9150_QIF_BYTE_SIZE 8 +#define DA9150_QIF_BYTE_MASK 0xFF +#define DA9150_QIF_SHORT_SIZE 2 +#define DA9150_QIF_LONG_SIZE 4 + +/* QIF Codes */ +#define DA9150_QIF_UAVG 6 +#define DA9150_QIF_UAVG_SIZE DA9150_QIF_LONG_SIZE +#define DA9150_QIF_IAVG 8 +#define DA9150_QIF_IAVG_SIZE DA9150_QIF_LONG_SIZE +#define DA9150_QIF_NTCAVG 12 +#define DA9150_QIF_NTCAVG_SIZE DA9150_QIF_LONG_SIZE +#define DA9150_QIF_SHUNT_VAL 36 +#define DA9150_QIF_SHUNT_VAL_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_SD_GAIN 38 +#define DA9150_QIF_SD_GAIN_SIZE DA9150_QIF_LONG_SIZE +#define DA9150_QIF_FCC_MAH 40 +#define DA9150_QIF_FCC_MAH_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_SOC_PCT 43 +#define DA9150_QIF_SOC_PCT_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_CHARGE_LIMIT 44 +#define DA9150_QIF_CHARGE_LIMIT_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_DISCHARGE_LIMIT 45 +#define DA9150_QIF_DISCHARGE_LIMIT_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_FW_MAIN_VER 118 +#define DA9150_QIF_FW_MAIN_VER_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_E_FG_STATUS 126 +#define DA9150_QIF_E_FG_STATUS_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_SYNC 127 +#define DA9150_QIF_SYNC_SIZE DA9150_QIF_SHORT_SIZE +#define DA9150_QIF_MAX_CODES 128 + +/* QIF Sync Timeout */ +#define DA9150_QIF_SYNC_TIMEOUT 1000 +#define DA9150_QIF_SYNC_RETRIES 10 + +/* QIF E_FG_STATUS */ +#define DA9150_FG_IRQ_LOW_SOC_MASK (1 << 0) +#define DA9150_FG_IRQ_HIGH_SOC_MASK (1 << 1) +#define DA9150_FG_IRQ_SOC_MASK \ + (DA9150_FG_IRQ_LOW_SOC_MASK | DA9150_FG_IRQ_HIGH_SOC_MASK) + +/* Private data */ +struct da9150_fg { + struct da9150 *da9150; + struct device *dev; + + struct mutex io_lock; + + struct power_supply *battery; + struct delayed_work work; + u32 interval; + + int warn_soc; + int crit_soc; + int soc; +}; + +/* Battery Properties */ +static u32 da9150_fg_read_attr(struct da9150_fg *fg, u8 code, u8 size) + +{ + u8 buf[DA9150_QIF_LONG_SIZE]; + u8 read_addr; + u32 res = 0; + int i; + + /* Set QIF code (READ mode) */ + read_addr = (code & DA9150_QIF_CODE_MASK) | DA9150_QIF_READ; + + da9150_read_qif(fg->da9150, read_addr, size, buf); + for (i = 0; i < size; ++i) + res |= (buf[i] << (i * DA9150_QIF_BYTE_SIZE)); + + return res; +} + +static void da9150_fg_write_attr(struct da9150_fg *fg, u8 code, u8 size, + u32 val) + +{ + u8 buf[DA9150_QIF_LONG_SIZE]; + u8 write_addr; + int i; + + /* Set QIF code (WRITE mode) */ + write_addr = (code & DA9150_QIF_CODE_MASK) | DA9150_QIF_WRITE; + + for (i = 0; i < size; ++i) { + buf[i] = (val >> (i * DA9150_QIF_BYTE_SIZE)) & + DA9150_QIF_BYTE_MASK; + } + da9150_write_qif(fg->da9150, write_addr, size, buf); +} + +/* Trigger QIF Sync to update QIF readable data */ +static void da9150_fg_read_sync_start(struct da9150_fg *fg) +{ + int i = 0; + u32 res = 0; + + mutex_lock(&fg->io_lock); + + /* Check if QIF sync already requested, and write to sync if not */ + res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE); + if (res > 0) + da9150_fg_write_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE, 0); + + /* Wait for sync to complete */ + res = 0; + while ((res == 0) && (i++ < DA9150_QIF_SYNC_RETRIES)) { + usleep_range(DA9150_QIF_SYNC_TIMEOUT, + DA9150_QIF_SYNC_TIMEOUT * 2); + res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE); + } + + /* Check if sync completed */ + if (res == 0) + dev_err(fg->dev, "Failed to perform QIF read sync!\n"); +} + +/* + * Should always be called after QIF sync read has been performed, and all + * attributes required have been accessed. + */ +static inline void da9150_fg_read_sync_end(struct da9150_fg *fg) +{ + mutex_unlock(&fg->io_lock); +} + +/* Sync read of single QIF attribute */ +static u32 da9150_fg_read_attr_sync(struct da9150_fg *fg, u8 code, u8 size) +{ + u32 val; + + da9150_fg_read_sync_start(fg); + val = da9150_fg_read_attr(fg, code, size); + da9150_fg_read_sync_end(fg); + + return val; +} + +/* Wait for QIF Sync, write QIF data and wait for ack */ +static void da9150_fg_write_attr_sync(struct da9150_fg *fg, u8 code, u8 size, + u32 val) +{ + int i = 0; + u32 res = 0, sync_val; + + mutex_lock(&fg->io_lock); + + /* Check if QIF sync already requested */ + res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE); + + /* Wait for an existing sync to complete */ + while ((res == 0) && (i++ < DA9150_QIF_SYNC_RETRIES)) { + usleep_range(DA9150_QIF_SYNC_TIMEOUT, + DA9150_QIF_SYNC_TIMEOUT * 2); + res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE); + } + + if (res == 0) { + dev_err(fg->dev, "Timeout waiting for existing QIF sync!\n"); + mutex_unlock(&fg->io_lock); + return; + } + + /* Write value for QIF code */ + da9150_fg_write_attr(fg, code, size, val); + + /* Wait for write acknowledgment */ + i = 0; + sync_val = res; + while ((res == sync_val) && (i++ < DA9150_QIF_SYNC_RETRIES)) { + usleep_range(DA9150_QIF_SYNC_TIMEOUT, + DA9150_QIF_SYNC_TIMEOUT * 2); + res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC, + DA9150_QIF_SYNC_SIZE); + } + + mutex_unlock(&fg->io_lock); + + /* Check write was actually successful */ + if (res != (sync_val + 1)) + dev_err(fg->dev, "Error performing QIF sync write for code %d\n", + code); +} + +/* Power Supply attributes */ +static int da9150_fg_capacity(struct da9150_fg *fg, + union power_supply_propval *val) +{ + val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_SOC_PCT, + DA9150_QIF_SOC_PCT_SIZE); + + if (val->intval > 100) + val->intval = 100; + + return 0; +} + +static int da9150_fg_current_avg(struct da9150_fg *fg, + union power_supply_propval *val) +{ + u32 iavg, sd_gain, shunt_val; + u64 div, res; + + da9150_fg_read_sync_start(fg); + iavg = da9150_fg_read_attr(fg, DA9150_QIF_IAVG, + DA9150_QIF_IAVG_SIZE); + shunt_val = da9150_fg_read_attr(fg, DA9150_QIF_SHUNT_VAL, + DA9150_QIF_SHUNT_VAL_SIZE); + sd_gain = da9150_fg_read_attr(fg, DA9150_QIF_SD_GAIN, + DA9150_QIF_SD_GAIN_SIZE); + da9150_fg_read_sync_end(fg); + + div = (u64) (sd_gain * shunt_val * 65536ULL); + do_div(div, 1000000); + res = (u64) (iavg * 1000000ULL); + do_div(res, div); + + val->intval = (int) res; + + return 0; +} + +static int da9150_fg_voltage_avg(struct da9150_fg *fg, + union power_supply_propval *val) +{ + u64 res; + + val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_UAVG, + DA9150_QIF_UAVG_SIZE); + + res = (u64) (val->intval * 186ULL); + do_div(res, 10000); + val->intval = (int) res; + + return 0; +} + +static int da9150_fg_charge_full(struct da9150_fg *fg, + union power_supply_propval *val) +{ + val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_FCC_MAH, + DA9150_QIF_FCC_MAH_SIZE); + + val->intval = val->intval * 1000; + + return 0; +} + +/* + * Temperature reading from device is only valid if battery/system provides + * valid NTC to associated pin of DA9150 chip. + */ +static int da9150_fg_temp(struct da9150_fg *fg, + union power_supply_propval *val) +{ + val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_NTCAVG, + DA9150_QIF_NTCAVG_SIZE); + + val->intval = (val->intval * 10) / 1048576; + + return 0; +} + +static enum power_supply_property da9150_fg_props[] = { + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_TEMP, +}; + +static int da9150_fg_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct da9150_fg *fg = dev_get_drvdata(psy->dev.parent); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY: + ret = da9150_fg_capacity(fg, val); + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = da9150_fg_current_avg(fg, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + ret = da9150_fg_voltage_avg(fg, val); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = da9150_fg_charge_full(fg, val); + break; + case POWER_SUPPLY_PROP_TEMP: + ret = da9150_fg_temp(fg, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +/* Repeated SOC check */ +static bool da9150_fg_soc_changed(struct da9150_fg *fg) +{ + union power_supply_propval val; + + da9150_fg_capacity(fg, &val); + if (val.intval != fg->soc) { + fg->soc = val.intval; + return true; + } + + return false; +} + +static void da9150_fg_work(struct work_struct *work) +{ + struct da9150_fg *fg = container_of(work, struct da9150_fg, work.work); + + /* Report if SOC has changed */ + if (da9150_fg_soc_changed(fg)) + power_supply_changed(fg->battery); + + schedule_delayed_work(&fg->work, msecs_to_jiffies(fg->interval)); +} + +/* SOC level event configuration */ +static void da9150_fg_soc_event_config(struct da9150_fg *fg) +{ + int soc; + + soc = da9150_fg_read_attr_sync(fg, DA9150_QIF_SOC_PCT, + DA9150_QIF_SOC_PCT_SIZE); + + if (soc > fg->warn_soc) { + /* If SOC > warn level, set discharge warn level event */ + da9150_fg_write_attr_sync(fg, DA9150_QIF_DISCHARGE_LIMIT, + DA9150_QIF_DISCHARGE_LIMIT_SIZE, + fg->warn_soc + 1); + } else if ((soc <= fg->warn_soc) && (soc > fg->crit_soc)) { + /* + * If SOC <= warn level, set discharge crit level event, + * and set charge warn level event. + */ + da9150_fg_write_attr_sync(fg, DA9150_QIF_DISCHARGE_LIMIT, + DA9150_QIF_DISCHARGE_LIMIT_SIZE, + fg->crit_soc + 1); + + da9150_fg_write_attr_sync(fg, DA9150_QIF_CHARGE_LIMIT, + DA9150_QIF_CHARGE_LIMIT_SIZE, + fg->warn_soc); + } else if (soc <= fg->crit_soc) { + /* If SOC <= crit level, set charge crit level event */ + da9150_fg_write_attr_sync(fg, DA9150_QIF_CHARGE_LIMIT, + DA9150_QIF_CHARGE_LIMIT_SIZE, + fg->crit_soc); + } +} + +static irqreturn_t da9150_fg_irq(int irq, void *data) +{ + struct da9150_fg *fg = data; + u32 e_fg_status; + + /* Read FG IRQ status info */ + e_fg_status = da9150_fg_read_attr(fg, DA9150_QIF_E_FG_STATUS, + DA9150_QIF_E_FG_STATUS_SIZE); + + /* Handle warning/critical threhold events */ + if (e_fg_status & DA9150_FG_IRQ_SOC_MASK) + da9150_fg_soc_event_config(fg); + + /* Clear any FG IRQs */ + da9150_fg_write_attr(fg, DA9150_QIF_E_FG_STATUS, + DA9150_QIF_E_FG_STATUS_SIZE, e_fg_status); + + return IRQ_HANDLED; +} + +static struct da9150_fg_pdata *da9150_fg_dt_pdata(struct device *dev) +{ + struct device_node *fg_node = dev->of_node; + struct da9150_fg_pdata *pdata; + + pdata = devm_kzalloc(dev, sizeof(struct da9150_fg_pdata), GFP_KERNEL); + if (!pdata) + return NULL; + + of_property_read_u32(fg_node, "dlg,update-interval", + &pdata->update_interval); + of_property_read_u8(fg_node, "dlg,warn-soc-level", + &pdata->warn_soc_lvl); + of_property_read_u8(fg_node, "dlg,crit-soc-level", + &pdata->crit_soc_lvl); + + return pdata; +} + +static const struct power_supply_desc fg_desc = { + .name = "da9150-fg", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = da9150_fg_props, + .num_properties = ARRAY_SIZE(da9150_fg_props), + .get_property = da9150_fg_get_prop, +}; + +static int da9150_fg_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct da9150 *da9150 = dev_get_drvdata(dev->parent); + struct da9150_fg_pdata *fg_pdata = dev_get_platdata(dev); + struct da9150_fg *fg; + int ver, irq, ret = 0; + + fg = devm_kzalloc(dev, sizeof(*fg), GFP_KERNEL); + if (fg == NULL) + return -ENOMEM; + + platform_set_drvdata(pdev, fg); + fg->da9150 = da9150; + fg->dev = dev; + + mutex_init(&fg->io_lock); + + /* Enable QIF */ + da9150_set_bits(da9150, DA9150_CORE2WIRE_CTRL_A, DA9150_FG_QIF_EN_MASK, + DA9150_FG_QIF_EN_MASK); + + fg->battery = devm_power_supply_register(dev, &fg_desc, NULL); + if (IS_ERR(fg->battery)) { + ret = PTR_ERR(fg->battery); + return ret; + } + + ver = da9150_fg_read_attr(fg, DA9150_QIF_FW_MAIN_VER, + DA9150_QIF_FW_MAIN_VER_SIZE); + dev_info(dev, "Version: 0x%x\n", ver); + + /* Handle DT data if provided */ + if (dev->of_node) { + fg_pdata = da9150_fg_dt_pdata(dev); + dev->platform_data = fg_pdata; + } + + /* Handle any pdata provided */ + if (fg_pdata) { + fg->interval = fg_pdata->update_interval; + + if (fg_pdata->warn_soc_lvl > 100) + dev_warn(dev, "Invalid SOC warning level provided, Ignoring"); + else + fg->warn_soc = fg_pdata->warn_soc_lvl; + + if ((fg_pdata->crit_soc_lvl > 100) || + (fg_pdata->crit_soc_lvl >= fg_pdata->warn_soc_lvl)) + dev_warn(dev, "Invalid SOC critical level provided, Ignoring"); + else + fg->crit_soc = fg_pdata->crit_soc_lvl; + + + } + + /* Configure initial SOC level events */ + da9150_fg_soc_event_config(fg); + + /* + * If an interval period has been provided then setup repeating + * work for reporting data updates. + */ + if (fg->interval) { + INIT_DELAYED_WORK(&fg->work, da9150_fg_work); + schedule_delayed_work(&fg->work, + msecs_to_jiffies(fg->interval)); + } + + /* Register IRQ */ + irq = platform_get_irq_byname(pdev, "FG"); + if (irq < 0) { + dev_err(dev, "Failed to get IRQ FG: %d\n", irq); + ret = irq; + goto irq_fail; + } + + ret = devm_request_threaded_irq(dev, irq, NULL, da9150_fg_irq, + IRQF_ONESHOT, "FG", fg); + if (ret) { + dev_err(dev, "Failed to request IRQ %d: %d\n", irq, ret); + goto irq_fail; + } + + return 0; + +irq_fail: + if (fg->interval) + cancel_delayed_work(&fg->work); + + return ret; +} + +static int da9150_fg_remove(struct platform_device *pdev) +{ + struct da9150_fg *fg = platform_get_drvdata(pdev); + + if (fg->interval) + cancel_delayed_work(&fg->work); + + return 0; +} + +static int da9150_fg_resume(struct platform_device *pdev) +{ + struct da9150_fg *fg = platform_get_drvdata(pdev); + + /* + * Trigger SOC check to happen now so as to indicate any value change + * since last check before suspend. + */ + if (fg->interval) + flush_delayed_work(&fg->work); + + return 0; +} + +static struct platform_driver da9150_fg_driver = { + .driver = { + .name = "da9150-fuel-gauge", + }, + .probe = da9150_fg_probe, + .remove = da9150_fg_remove, + .resume = da9150_fg_resume, +}; + +module_platform_driver(da9150_fg_driver); + +MODULE_DESCRIPTION("Fuel-Gauge Driver for DA9150"); +MODULE_AUTHOR("Adam Thomson <Adam.Thomson.Opensource@diasemi.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/ds2760_battery.c b/drivers/power/supply/ds2760_battery.c new file mode 100644 index 000000000..695bb6747 --- /dev/null +++ b/drivers/power/supply/ds2760_battery.c @@ -0,0 +1,816 @@ +/* + * Driver for batteries with DS2760 chips inside. + * + * Copyright © 2007 Anton Vorontsov + * 2004-2007 Matt Reimer + * 2004 Szabolcs Gyurko + * + * Use consistent with the GNU GPL is permitted, + * provided that this copyright notice is + * preserved in its entirety in all copies and derived works. + * + * Author: Anton Vorontsov <cbou@mail.ru> + * February 2007 + * + * Matt Reimer <mreimer@vpop.net> + * April 2004, 2005, 2007 + * + * Szabolcs Gyurko <szabolcs.gyurko@tlt.hu> + * September 2004 + */ + +#include <linux/module.h> +#include <linux/param.h> +#include <linux/jiffies.h> +#include <linux/workqueue.h> +#include <linux/pm.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/suspend.h> +#include <linux/w1.h> +#include <linux/of.h> + +static unsigned int cache_time = 1000; +module_param(cache_time, uint, 0644); +MODULE_PARM_DESC(cache_time, "cache time in milliseconds"); + +static bool pmod_enabled; +module_param(pmod_enabled, bool, 0644); +MODULE_PARM_DESC(pmod_enabled, "PMOD enable bit"); + +static unsigned int rated_capacity; +module_param(rated_capacity, uint, 0644); +MODULE_PARM_DESC(rated_capacity, "rated battery capacity, 10*mAh or index"); + +static unsigned int current_accum; +module_param(current_accum, uint, 0644); +MODULE_PARM_DESC(current_accum, "current accumulator value"); + +#define W1_FAMILY_DS2760 0x30 + +/* Known commands to the DS2760 chip */ +#define W1_DS2760_SWAP 0xAA +#define W1_DS2760_READ_DATA 0x69 +#define W1_DS2760_WRITE_DATA 0x6C +#define W1_DS2760_COPY_DATA 0x48 +#define W1_DS2760_RECALL_DATA 0xB8 +#define W1_DS2760_LOCK 0x6A + +/* Number of valid register addresses */ +#define DS2760_DATA_SIZE 0x40 + +#define DS2760_PROTECTION_REG 0x00 + +#define DS2760_STATUS_REG 0x01 +#define DS2760_STATUS_IE (1 << 2) +#define DS2760_STATUS_SWEN (1 << 3) +#define DS2760_STATUS_RNAOP (1 << 4) +#define DS2760_STATUS_PMOD (1 << 5) + +#define DS2760_EEPROM_REG 0x07 +#define DS2760_SPECIAL_FEATURE_REG 0x08 +#define DS2760_VOLTAGE_MSB 0x0c +#define DS2760_VOLTAGE_LSB 0x0d +#define DS2760_CURRENT_MSB 0x0e +#define DS2760_CURRENT_LSB 0x0f +#define DS2760_CURRENT_ACCUM_MSB 0x10 +#define DS2760_CURRENT_ACCUM_LSB 0x11 +#define DS2760_TEMP_MSB 0x18 +#define DS2760_TEMP_LSB 0x19 +#define DS2760_EEPROM_BLOCK0 0x20 +#define DS2760_ACTIVE_FULL 0x20 +#define DS2760_EEPROM_BLOCK1 0x30 +#define DS2760_STATUS_WRITE_REG 0x31 +#define DS2760_RATED_CAPACITY 0x32 +#define DS2760_CURRENT_OFFSET_BIAS 0x33 +#define DS2760_ACTIVE_EMPTY 0x3b + +struct ds2760_device_info { + struct device *dev; + + /* DS2760 data, valid after calling ds2760_battery_read_status() */ + unsigned long update_time; /* jiffies when data read */ + char raw[DS2760_DATA_SIZE]; /* raw DS2760 data */ + int voltage_raw; /* units of 4.88 mV */ + int voltage_uV; /* units of µV */ + int current_raw; /* units of 0.625 mA */ + int current_uA; /* units of µA */ + int accum_current_raw; /* units of 0.25 mAh */ + int accum_current_uAh; /* units of µAh */ + int temp_raw; /* units of 0.125 °C */ + int temp_C; /* units of 0.1 °C */ + int rated_capacity; /* units of µAh */ + int rem_capacity; /* percentage */ + int full_active_uAh; /* units of µAh */ + int empty_uAh; /* units of µAh */ + int life_sec; /* units of seconds */ + int charge_status; /* POWER_SUPPLY_STATUS_* */ + + int full_counter; + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct workqueue_struct *monitor_wqueue; + struct delayed_work monitor_work; + struct delayed_work set_charged_work; + struct notifier_block pm_notifier; +}; + +static int w1_ds2760_io(struct device *dev, char *buf, int addr, size_t count, + int io) +{ + struct w1_slave *sl = container_of(dev, struct w1_slave, dev); + + if (!dev) + return 0; + + mutex_lock(&sl->master->bus_mutex); + + if (addr > DS2760_DATA_SIZE || addr < 0) { + count = 0; + goto out; + } + if (addr + count > DS2760_DATA_SIZE) + count = DS2760_DATA_SIZE - addr; + + if (!w1_reset_select_slave(sl)) { + if (!io) { + w1_write_8(sl->master, W1_DS2760_READ_DATA); + w1_write_8(sl->master, addr); + count = w1_read_block(sl->master, buf, count); + } else { + w1_write_8(sl->master, W1_DS2760_WRITE_DATA); + w1_write_8(sl->master, addr); + w1_write_block(sl->master, buf, count); + /* XXX w1_write_block returns void, not n_written */ + } + } + +out: + mutex_unlock(&sl->master->bus_mutex); + + return count; +} + +static int w1_ds2760_read(struct device *dev, + char *buf, int addr, + size_t count) +{ + return w1_ds2760_io(dev, buf, addr, count, 0); +} + +static int w1_ds2760_write(struct device *dev, + char *buf, + int addr, size_t count) +{ + return w1_ds2760_io(dev, buf, addr, count, 1); +} + +static int w1_ds2760_eeprom_cmd(struct device *dev, int addr, int cmd) +{ + struct w1_slave *sl = container_of(dev, struct w1_slave, dev); + + if (!dev) + return -EINVAL; + + mutex_lock(&sl->master->bus_mutex); + + if (w1_reset_select_slave(sl) == 0) { + w1_write_8(sl->master, cmd); + w1_write_8(sl->master, addr); + } + + mutex_unlock(&sl->master->bus_mutex); + return 0; +} + +static int w1_ds2760_store_eeprom(struct device *dev, int addr) +{ + return w1_ds2760_eeprom_cmd(dev, addr, W1_DS2760_COPY_DATA); +} + +static int w1_ds2760_recall_eeprom(struct device *dev, int addr) +{ + return w1_ds2760_eeprom_cmd(dev, addr, W1_DS2760_RECALL_DATA); +} + +static ssize_t w1_slave_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *bin_attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + return w1_ds2760_read(dev, buf, off, count); +} + +static BIN_ATTR_RO(w1_slave, DS2760_DATA_SIZE); + +static struct bin_attribute *w1_ds2760_bin_attrs[] = { + &bin_attr_w1_slave, + NULL, +}; + +static const struct attribute_group w1_ds2760_group = { + .bin_attrs = w1_ds2760_bin_attrs, +}; + +static const struct attribute_group *w1_ds2760_groups[] = { + &w1_ds2760_group, + NULL, +}; +/* Some batteries have their rated capacity stored a N * 10 mAh, while + * others use an index into this table. */ +static int rated_capacities[] = { + 0, + 920, /* Samsung */ + 920, /* BYD */ + 920, /* Lishen */ + 920, /* NEC */ + 1440, /* Samsung */ + 1440, /* BYD */ +#ifdef CONFIG_MACH_H4700 + 1800, /* HP iPAQ hx4700 3.7V 1800mAh (359113-001) */ +#else + 1440, /* Lishen */ +#endif + 1440, /* NEC */ + 2880, /* Samsung */ + 2880, /* BYD */ + 2880, /* Lishen */ + 2880, /* NEC */ +#ifdef CONFIG_MACH_H4700 + 0, + 3600, /* HP iPAQ hx4700 3.7V 3600mAh (359114-001) */ +#endif +}; + +/* array is level at temps 0°C, 10°C, 20°C, 30°C, 40°C + * temp is in Celsius */ +static int battery_interpolate(int array[], int temp) +{ + int index, dt; + + if (temp <= 0) + return array[0]; + if (temp >= 40) + return array[4]; + + index = temp / 10; + dt = temp % 10; + + return array[index] + (((array[index + 1] - array[index]) * dt) / 10); +} + +static int ds2760_battery_read_status(struct ds2760_device_info *di) +{ + int ret, i, start, count, scale[5]; + + if (di->update_time && time_before(jiffies, di->update_time + + msecs_to_jiffies(cache_time))) + return 0; + + /* The first time we read the entire contents of SRAM/EEPROM, + * but after that we just read the interesting bits that change. */ + if (di->update_time == 0) { + start = 0; + count = DS2760_DATA_SIZE; + } else { + start = DS2760_VOLTAGE_MSB; + count = DS2760_TEMP_LSB - start + 1; + } + + ret = w1_ds2760_read(di->dev, di->raw + start, start, count); + if (ret != count) { + dev_warn(di->dev, "call to w1_ds2760_read failed (0x%p)\n", + di->dev); + return 1; + } + + di->update_time = jiffies; + + /* DS2760 reports voltage in units of 4.88mV, but the battery class + * reports in units of uV, so convert by multiplying by 4880. */ + di->voltage_raw = (di->raw[DS2760_VOLTAGE_MSB] << 3) | + (di->raw[DS2760_VOLTAGE_LSB] >> 5); + di->voltage_uV = di->voltage_raw * 4880; + + /* DS2760 reports current in signed units of 0.625mA, but the battery + * class reports in units of µA, so convert by multiplying by 625. */ + di->current_raw = + (((signed char)di->raw[DS2760_CURRENT_MSB]) << 5) | + (di->raw[DS2760_CURRENT_LSB] >> 3); + di->current_uA = di->current_raw * 625; + + /* DS2760 reports accumulated current in signed units of 0.25mAh. */ + di->accum_current_raw = + (((signed char)di->raw[DS2760_CURRENT_ACCUM_MSB]) << 8) | + di->raw[DS2760_CURRENT_ACCUM_LSB]; + di->accum_current_uAh = di->accum_current_raw * 250; + + /* DS2760 reports temperature in signed units of 0.125°C, but the + * battery class reports in units of 1/10 °C, so we convert by + * multiplying by .125 * 10 = 1.25. */ + di->temp_raw = (((signed char)di->raw[DS2760_TEMP_MSB]) << 3) | + (di->raw[DS2760_TEMP_LSB] >> 5); + di->temp_C = di->temp_raw + (di->temp_raw / 4); + + /* At least some battery monitors (e.g. HP iPAQ) store the battery's + * maximum rated capacity. */ + if (di->raw[DS2760_RATED_CAPACITY] < ARRAY_SIZE(rated_capacities)) + di->rated_capacity = rated_capacities[ + (unsigned int)di->raw[DS2760_RATED_CAPACITY]]; + else + di->rated_capacity = di->raw[DS2760_RATED_CAPACITY] * 10; + + di->rated_capacity *= 1000; /* convert to µAh */ + + /* Calculate the full level at the present temperature. */ + di->full_active_uAh = di->raw[DS2760_ACTIVE_FULL] << 8 | + di->raw[DS2760_ACTIVE_FULL + 1]; + + /* If the full_active_uAh value is not given, fall back to the rated + * capacity. This is likely to happen when chips are not part of the + * battery pack and is therefore not bootstrapped. */ + if (di->full_active_uAh == 0) + di->full_active_uAh = di->rated_capacity / 1000L; + + scale[0] = di->full_active_uAh; + for (i = 1; i < 5; i++) + scale[i] = scale[i - 1] + di->raw[DS2760_ACTIVE_FULL + 1 + i]; + + di->full_active_uAh = battery_interpolate(scale, di->temp_C / 10); + di->full_active_uAh *= 1000; /* convert to µAh */ + + /* Calculate the empty level at the present temperature. */ + scale[4] = di->raw[DS2760_ACTIVE_EMPTY + 4]; + for (i = 3; i >= 0; i--) + scale[i] = scale[i + 1] + di->raw[DS2760_ACTIVE_EMPTY + i]; + + di->empty_uAh = battery_interpolate(scale, di->temp_C / 10); + di->empty_uAh *= 1000; /* convert to µAh */ + + if (di->full_active_uAh == di->empty_uAh) + di->rem_capacity = 0; + else + /* From Maxim Application Note 131: remaining capacity = + * ((ICA - Empty Value) / (Full Value - Empty Value)) x 100% */ + di->rem_capacity = ((di->accum_current_uAh - di->empty_uAh) * 100L) / + (di->full_active_uAh - di->empty_uAh); + + if (di->rem_capacity < 0) + di->rem_capacity = 0; + if (di->rem_capacity > 100) + di->rem_capacity = 100; + + if (di->current_uA < -100L) + di->life_sec = -((di->accum_current_uAh - di->empty_uAh) * 36L) + / (di->current_uA / 100L); + else + di->life_sec = 0; + + return 0; +} + +static void ds2760_battery_set_current_accum(struct ds2760_device_info *di, + unsigned int acr_val) +{ + unsigned char acr[2]; + + /* acr is in units of 0.25 mAh */ + acr_val *= 4L; + acr_val /= 1000; + + acr[0] = acr_val >> 8; + acr[1] = acr_val & 0xff; + + if (w1_ds2760_write(di->dev, acr, DS2760_CURRENT_ACCUM_MSB, 2) < 2) + dev_warn(di->dev, "ACR write failed\n"); +} + +static void ds2760_battery_update_status(struct ds2760_device_info *di) +{ + int old_charge_status = di->charge_status; + + ds2760_battery_read_status(di); + + if (di->charge_status == POWER_SUPPLY_STATUS_UNKNOWN) + di->full_counter = 0; + + if (power_supply_am_i_supplied(di->bat)) { + if (di->current_uA > 10000) { + di->charge_status = POWER_SUPPLY_STATUS_CHARGING; + di->full_counter = 0; + } else if (di->current_uA < -5000) { + if (di->charge_status != POWER_SUPPLY_STATUS_NOT_CHARGING) + dev_notice(di->dev, "not enough power to " + "charge\n"); + di->charge_status = POWER_SUPPLY_STATUS_NOT_CHARGING; + di->full_counter = 0; + } else if (di->current_uA < 10000 && + di->charge_status != POWER_SUPPLY_STATUS_FULL) { + + /* Don't consider the battery to be full unless + * we've seen the current < 10 mA at least two + * consecutive times. */ + + di->full_counter++; + + if (di->full_counter < 2) { + di->charge_status = POWER_SUPPLY_STATUS_CHARGING; + } else { + di->charge_status = POWER_SUPPLY_STATUS_FULL; + ds2760_battery_set_current_accum(di, + di->full_active_uAh); + } + } + } else { + di->charge_status = POWER_SUPPLY_STATUS_DISCHARGING; + di->full_counter = 0; + } + + if (di->charge_status != old_charge_status) + power_supply_changed(di->bat); +} + +static void ds2760_battery_write_status(struct ds2760_device_info *di, + char status) +{ + if (status == di->raw[DS2760_STATUS_REG]) + return; + + w1_ds2760_write(di->dev, &status, DS2760_STATUS_WRITE_REG, 1); + w1_ds2760_store_eeprom(di->dev, DS2760_EEPROM_BLOCK1); + w1_ds2760_recall_eeprom(di->dev, DS2760_EEPROM_BLOCK1); +} + +static void ds2760_battery_write_rated_capacity(struct ds2760_device_info *di, + unsigned char rated_capacity) +{ + if (rated_capacity == di->raw[DS2760_RATED_CAPACITY]) + return; + + w1_ds2760_write(di->dev, &rated_capacity, DS2760_RATED_CAPACITY, 1); + w1_ds2760_store_eeprom(di->dev, DS2760_EEPROM_BLOCK1); + w1_ds2760_recall_eeprom(di->dev, DS2760_EEPROM_BLOCK1); +} + +static void ds2760_battery_write_active_full(struct ds2760_device_info *di, + int active_full) +{ + unsigned char tmp[2] = { + active_full >> 8, + active_full & 0xff + }; + + if (tmp[0] == di->raw[DS2760_ACTIVE_FULL] && + tmp[1] == di->raw[DS2760_ACTIVE_FULL + 1]) + return; + + w1_ds2760_write(di->dev, tmp, DS2760_ACTIVE_FULL, sizeof(tmp)); + w1_ds2760_store_eeprom(di->dev, DS2760_EEPROM_BLOCK0); + w1_ds2760_recall_eeprom(di->dev, DS2760_EEPROM_BLOCK0); + + /* Write to the di->raw[] buffer directly - the DS2760_ACTIVE_FULL + * values won't be read back by ds2760_battery_read_status() */ + di->raw[DS2760_ACTIVE_FULL] = tmp[0]; + di->raw[DS2760_ACTIVE_FULL + 1] = tmp[1]; +} + +static void ds2760_battery_work(struct work_struct *work) +{ + struct ds2760_device_info *di = container_of(work, + struct ds2760_device_info, monitor_work.work); + const int interval = HZ * 60; + + dev_dbg(di->dev, "%s\n", __func__); + + ds2760_battery_update_status(di); + queue_delayed_work(di->monitor_wqueue, &di->monitor_work, interval); +} + +static void ds2760_battery_external_power_changed(struct power_supply *psy) +{ + struct ds2760_device_info *di = power_supply_get_drvdata(psy); + + dev_dbg(di->dev, "%s\n", __func__); + + mod_delayed_work(di->monitor_wqueue, &di->monitor_work, HZ/10); +} + + +static void ds2760_battery_set_charged_work(struct work_struct *work) +{ + char bias; + struct ds2760_device_info *di = container_of(work, + struct ds2760_device_info, set_charged_work.work); + + dev_dbg(di->dev, "%s\n", __func__); + + ds2760_battery_read_status(di); + + /* When we get notified by external circuitry that the battery is + * considered fully charged now, we know that there is no current + * flow any more. However, the ds2760's internal current meter is + * too inaccurate to rely on - spec say something ~15% failure. + * Hence, we use the current offset bias register to compensate + * that error. + */ + + if (!power_supply_am_i_supplied(di->bat)) + return; + + bias = (signed char) di->current_raw + + (signed char) di->raw[DS2760_CURRENT_OFFSET_BIAS]; + + dev_dbg(di->dev, "%s: bias = %d\n", __func__, bias); + + w1_ds2760_write(di->dev, &bias, DS2760_CURRENT_OFFSET_BIAS, 1); + w1_ds2760_store_eeprom(di->dev, DS2760_EEPROM_BLOCK1); + w1_ds2760_recall_eeprom(di->dev, DS2760_EEPROM_BLOCK1); + + /* Write to the di->raw[] buffer directly - the CURRENT_OFFSET_BIAS + * value won't be read back by ds2760_battery_read_status() */ + di->raw[DS2760_CURRENT_OFFSET_BIAS] = bias; +} + +static void ds2760_battery_set_charged(struct power_supply *psy) +{ + struct ds2760_device_info *di = power_supply_get_drvdata(psy); + + /* postpone the actual work by 20 secs. This is for debouncing GPIO + * signals and to let the current value settle. See AN4188. */ + mod_delayed_work(di->monitor_wqueue, &di->set_charged_work, HZ * 20); +} + +static int ds2760_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ds2760_device_info *di = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = di->charge_status; + return 0; + default: + break; + } + + ds2760_battery_read_status(di); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = di->voltage_uV; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = di->current_uA; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = di->rated_capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = di->full_active_uAh; + break; + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + val->intval = di->empty_uAh; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = di->accum_current_uAh; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = di->temp_C; + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + val->intval = di->life_sec; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = di->rem_capacity; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int ds2760_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct ds2760_device_info *di = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + /* the interface counts in uAh, convert the value */ + ds2760_battery_write_active_full(di, val->intval / 1000L); + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + /* ds2760_battery_set_current_accum() does the conversion */ + ds2760_battery_set_current_accum(di, val->intval); + break; + + default: + return -EPERM; + } + + return 0; +} + +static int ds2760_battery_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_NOW: + return 1; + + default: + break; + } + + return 0; +} + +static enum power_supply_property ds2760_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_EMPTY, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_CAPACITY, +}; + +static int ds2760_pm_notifier(struct notifier_block *notifier, + unsigned long pm_event, + void *unused) +{ + struct ds2760_device_info *di = + container_of(notifier, struct ds2760_device_info, pm_notifier); + + switch (pm_event) { + case PM_HIBERNATION_PREPARE: + case PM_SUSPEND_PREPARE: + di->charge_status = POWER_SUPPLY_STATUS_UNKNOWN; + break; + + case PM_POST_RESTORE: + case PM_POST_HIBERNATION: + case PM_POST_SUSPEND: + di->charge_status = POWER_SUPPLY_STATUS_UNKNOWN; + power_supply_changed(di->bat); + mod_delayed_work(di->monitor_wqueue, &di->monitor_work, HZ); + + break; + + case PM_RESTORE_PREPARE: + default: + break; + } + + return NOTIFY_DONE; +} + +static int w1_ds2760_add_slave(struct w1_slave *sl) +{ + struct power_supply_config psy_cfg = {}; + struct ds2760_device_info *di; + struct device *dev = &sl->dev; + int retval = 0; + char name[32]; + char status; + + di = devm_kzalloc(dev, sizeof(*di), GFP_KERNEL); + if (!di) { + retval = -ENOMEM; + goto di_alloc_failed; + } + + snprintf(name, sizeof(name), "ds2760-battery.%d", dev->id); + + di->dev = dev; + di->bat_desc.name = name; + di->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY; + di->bat_desc.properties = ds2760_battery_props; + di->bat_desc.num_properties = ARRAY_SIZE(ds2760_battery_props); + di->bat_desc.get_property = ds2760_battery_get_property; + di->bat_desc.set_property = ds2760_battery_set_property; + di->bat_desc.property_is_writeable = + ds2760_battery_property_is_writeable; + di->bat_desc.set_charged = ds2760_battery_set_charged; + di->bat_desc.external_power_changed = + ds2760_battery_external_power_changed; + + psy_cfg.drv_data = di; + + if (dev->of_node) { + u32 tmp; + + psy_cfg.of_node = dev->of_node; + + if (!of_property_read_bool(dev->of_node, "maxim,pmod-enabled")) + pmod_enabled = true; + + if (!of_property_read_u32(dev->of_node, + "maxim,cache-time-ms", &tmp)) + cache_time = tmp; + + if (!of_property_read_u32(dev->of_node, + "rated-capacity-microamp-hours", + &tmp)) + rated_capacity = tmp / 10; /* property is in mAh */ + } + + di->charge_status = POWER_SUPPLY_STATUS_UNKNOWN; + + sl->family_data = di; + + /* enable sleep mode feature */ + ds2760_battery_read_status(di); + status = di->raw[DS2760_STATUS_REG]; + if (pmod_enabled) + status |= DS2760_STATUS_PMOD; + else + status &= ~DS2760_STATUS_PMOD; + + ds2760_battery_write_status(di, status); + + /* set rated capacity from module param or device tree */ + if (rated_capacity) + ds2760_battery_write_rated_capacity(di, rated_capacity); + + /* set current accumulator if given as parameter. + * this should only be done for bootstrapping the value */ + if (current_accum) + ds2760_battery_set_current_accum(di, current_accum); + + di->bat = power_supply_register(dev, &di->bat_desc, &psy_cfg); + if (IS_ERR(di->bat)) { + dev_err(di->dev, "failed to register battery\n"); + retval = PTR_ERR(di->bat); + goto batt_failed; + } + + INIT_DELAYED_WORK(&di->monitor_work, ds2760_battery_work); + INIT_DELAYED_WORK(&di->set_charged_work, + ds2760_battery_set_charged_work); + di->monitor_wqueue = alloc_ordered_workqueue(name, WQ_MEM_RECLAIM); + if (!di->monitor_wqueue) { + retval = -ESRCH; + goto workqueue_failed; + } + queue_delayed_work(di->monitor_wqueue, &di->monitor_work, HZ * 1); + + di->pm_notifier.notifier_call = ds2760_pm_notifier; + register_pm_notifier(&di->pm_notifier); + + goto success; + +workqueue_failed: + power_supply_unregister(di->bat); +batt_failed: +di_alloc_failed: +success: + return retval; +} + +static void w1_ds2760_remove_slave(struct w1_slave *sl) +{ + struct ds2760_device_info *di = sl->family_data; + + unregister_pm_notifier(&di->pm_notifier); + cancel_delayed_work_sync(&di->monitor_work); + cancel_delayed_work_sync(&di->set_charged_work); + destroy_workqueue(di->monitor_wqueue); + power_supply_unregister(di->bat); +} + +#ifdef CONFIG_OF +static const struct of_device_id w1_ds2760_of_ids[] = { + { .compatible = "maxim,ds2760" }, + {} +}; +#endif + +static const struct w1_family_ops w1_ds2760_fops = { + .add_slave = w1_ds2760_add_slave, + .remove_slave = w1_ds2760_remove_slave, + .groups = w1_ds2760_groups, +}; + +static struct w1_family w1_ds2760_family = { + .fid = W1_FAMILY_DS2760, + .fops = &w1_ds2760_fops, + .of_match_table = of_match_ptr(w1_ds2760_of_ids), +}; +module_w1_family(w1_ds2760_family); + +MODULE_AUTHOR("Szabolcs Gyurko <szabolcs.gyurko@tlt.hu>, " + "Matt Reimer <mreimer@vpop.net>, " + "Anton Vorontsov <cbou@mail.ru>"); +MODULE_DESCRIPTION("1-wire Driver Dallas 2760 battery monitor chip"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("w1-family-" __stringify(W1_FAMILY_DS2760)); diff --git a/drivers/power/supply/ds2780_battery.c b/drivers/power/supply/ds2780_battery.c new file mode 100644 index 000000000..dd57a472e --- /dev/null +++ b/drivers/power/supply/ds2780_battery.c @@ -0,0 +1,790 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * 1-wire client/driver for the Maxim/Dallas DS2780 Stand-Alone Fuel Gauge IC + * + * Copyright (C) 2010 Indesign, LLC + * + * Author: Clifton Barnes <cabarnes@indesign-llc.com> + * + * Based on ds2760_battery and ds2782_battery drivers + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/param.h> +#include <linux/pm.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/idr.h> + +#include <linux/w1.h> +#include "../../w1/slaves/w1_ds2780.h" + +/* Current unit measurement in uA for a 1 milli-ohm sense resistor */ +#define DS2780_CURRENT_UNITS 1563 +/* Charge unit measurement in uAh for a 1 milli-ohm sense resistor */ +#define DS2780_CHARGE_UNITS 6250 +/* Number of bytes in user EEPROM space */ +#define DS2780_USER_EEPROM_SIZE (DS2780_EEPROM_BLOCK0_END - \ + DS2780_EEPROM_BLOCK0_START + 1) +/* Number of bytes in parameter EEPROM space */ +#define DS2780_PARAM_EEPROM_SIZE (DS2780_EEPROM_BLOCK1_END - \ + DS2780_EEPROM_BLOCK1_START + 1) + +struct ds2780_device_info { + struct device *dev; + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct device *w1_dev; +}; + +enum current_types { + CURRENT_NOW, + CURRENT_AVG, +}; + +static const char model[] = "DS2780"; +static const char manufacturer[] = "Maxim/Dallas"; + +static inline struct ds2780_device_info * +to_ds2780_device_info(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static inline int ds2780_battery_io(struct ds2780_device_info *dev_info, + char *buf, int addr, size_t count, int io) +{ + return w1_ds2780_io(dev_info->w1_dev, buf, addr, count, io); +} + +static inline int ds2780_read8(struct ds2780_device_info *dev_info, u8 *val, + int addr) +{ + return ds2780_battery_io(dev_info, val, addr, sizeof(u8), 0); +} + +static int ds2780_read16(struct ds2780_device_info *dev_info, s16 *val, + int addr) +{ + int ret; + u8 raw[2]; + + ret = ds2780_battery_io(dev_info, raw, addr, sizeof(raw), 0); + if (ret < 0) + return ret; + + *val = (raw[0] << 8) | raw[1]; + + return 0; +} + +static inline int ds2780_read_block(struct ds2780_device_info *dev_info, + u8 *val, int addr, size_t count) +{ + return ds2780_battery_io(dev_info, val, addr, count, 0); +} + +static inline int ds2780_write(struct ds2780_device_info *dev_info, u8 *val, + int addr, size_t count) +{ + return ds2780_battery_io(dev_info, val, addr, count, 1); +} + +static inline int ds2780_store_eeprom(struct device *dev, int addr) +{ + return w1_ds2780_eeprom_cmd(dev, addr, W1_DS2780_COPY_DATA); +} + +static inline int ds2780_recall_eeprom(struct device *dev, int addr) +{ + return w1_ds2780_eeprom_cmd(dev, addr, W1_DS2780_RECALL_DATA); +} + +static int ds2780_save_eeprom(struct ds2780_device_info *dev_info, int reg) +{ + int ret; + + ret = ds2780_store_eeprom(dev_info->w1_dev, reg); + if (ret < 0) + return ret; + + ret = ds2780_recall_eeprom(dev_info->w1_dev, reg); + if (ret < 0) + return ret; + + return 0; +} + +/* Set sense resistor value in mhos */ +static int ds2780_set_sense_register(struct ds2780_device_info *dev_info, + u8 conductance) +{ + int ret; + + ret = ds2780_write(dev_info, &conductance, + DS2780_RSNSP_REG, sizeof(u8)); + if (ret < 0) + return ret; + + return ds2780_save_eeprom(dev_info, DS2780_RSNSP_REG); +} + +/* Get RSGAIN value from 0 to 1.999 in steps of 0.001 */ +static int ds2780_get_rsgain_register(struct ds2780_device_info *dev_info, + u16 *rsgain) +{ + return ds2780_read16(dev_info, rsgain, DS2780_RSGAIN_MSB_REG); +} + +/* Set RSGAIN value from 0 to 1.999 in steps of 0.001 */ +static int ds2780_set_rsgain_register(struct ds2780_device_info *dev_info, + u16 rsgain) +{ + int ret; + u8 raw[] = {rsgain >> 8, rsgain & 0xFF}; + + ret = ds2780_write(dev_info, raw, + DS2780_RSGAIN_MSB_REG, sizeof(raw)); + if (ret < 0) + return ret; + + return ds2780_save_eeprom(dev_info, DS2780_RSGAIN_MSB_REG); +} + +static int ds2780_get_voltage(struct ds2780_device_info *dev_info, + int *voltage_uV) +{ + int ret; + s16 voltage_raw; + + /* + * The voltage value is located in 10 bits across the voltage MSB + * and LSB registers in two's complement form + * Sign bit of the voltage value is in bit 7 of the voltage MSB register + * Bits 9 - 3 of the voltage value are in bits 6 - 0 of the + * voltage MSB register + * Bits 2 - 0 of the voltage value are in bits 7 - 5 of the + * voltage LSB register + */ + ret = ds2780_read16(dev_info, &voltage_raw, + DS2780_VOLT_MSB_REG); + if (ret < 0) + return ret; + + /* + * DS2780 reports voltage in units of 4.88mV, but the battery class + * reports in units of uV, so convert by multiplying by 4880. + */ + *voltage_uV = (voltage_raw / 32) * 4880; + return 0; +} + +static int ds2780_get_temperature(struct ds2780_device_info *dev_info, + int *temperature) +{ + int ret; + s16 temperature_raw; + + /* + * The temperature value is located in 10 bits across the temperature + * MSB and LSB registers in two's complement form + * Sign bit of the temperature value is in bit 7 of the temperature + * MSB register + * Bits 9 - 3 of the temperature value are in bits 6 - 0 of the + * temperature MSB register + * Bits 2 - 0 of the temperature value are in bits 7 - 5 of the + * temperature LSB register + */ + ret = ds2780_read16(dev_info, &temperature_raw, + DS2780_TEMP_MSB_REG); + if (ret < 0) + return ret; + + /* + * Temperature is measured in units of 0.125 degrees celcius, the + * power_supply class measures temperature in tenths of degrees + * celsius. The temperature value is stored as a 10 bit number, plus + * sign in the upper bits of a 16 bit register. + */ + *temperature = ((temperature_raw / 32) * 125) / 100; + return 0; +} + +static int ds2780_get_current(struct ds2780_device_info *dev_info, + enum current_types type, int *current_uA) +{ + int ret, sense_res; + s16 current_raw; + u8 sense_res_raw, reg_msb; + + /* + * The units of measurement for current are dependent on the value of + * the sense resistor. + */ + ret = ds2780_read8(dev_info, &sense_res_raw, DS2780_RSNSP_REG); + if (ret < 0) + return ret; + + if (sense_res_raw == 0) { + dev_err(dev_info->dev, "sense resistor value is 0\n"); + return -EINVAL; + } + sense_res = 1000 / sense_res_raw; + + if (type == CURRENT_NOW) + reg_msb = DS2780_CURRENT_MSB_REG; + else if (type == CURRENT_AVG) + reg_msb = DS2780_IAVG_MSB_REG; + else + return -EINVAL; + + /* + * The current value is located in 16 bits across the current MSB + * and LSB registers in two's complement form + * Sign bit of the current value is in bit 7 of the current MSB register + * Bits 14 - 8 of the current value are in bits 6 - 0 of the current + * MSB register + * Bits 7 - 0 of the current value are in bits 7 - 0 of the current + * LSB register + */ + ret = ds2780_read16(dev_info, ¤t_raw, reg_msb); + if (ret < 0) + return ret; + + *current_uA = current_raw * (DS2780_CURRENT_UNITS / sense_res); + return 0; +} + +static int ds2780_get_accumulated_current(struct ds2780_device_info *dev_info, + int *accumulated_current) +{ + int ret, sense_res; + s16 current_raw; + u8 sense_res_raw; + + /* + * The units of measurement for accumulated current are dependent on + * the value of the sense resistor. + */ + ret = ds2780_read8(dev_info, &sense_res_raw, DS2780_RSNSP_REG); + if (ret < 0) + return ret; + + if (sense_res_raw == 0) { + dev_err(dev_info->dev, "sense resistor value is 0\n"); + return -ENXIO; + } + sense_res = 1000 / sense_res_raw; + + /* + * The ACR value is located in 16 bits across the ACR MSB and + * LSB registers + * Bits 15 - 8 of the ACR value are in bits 7 - 0 of the ACR + * MSB register + * Bits 7 - 0 of the ACR value are in bits 7 - 0 of the ACR + * LSB register + */ + ret = ds2780_read16(dev_info, ¤t_raw, DS2780_ACR_MSB_REG); + if (ret < 0) + return ret; + + *accumulated_current = current_raw * (DS2780_CHARGE_UNITS / sense_res); + return 0; +} + +static int ds2780_get_capacity(struct ds2780_device_info *dev_info, + int *capacity) +{ + int ret; + u8 raw; + + ret = ds2780_read8(dev_info, &raw, DS2780_RARC_REG); + if (ret < 0) + return ret; + + *capacity = raw; + return raw; +} + +static int ds2780_get_status(struct ds2780_device_info *dev_info, int *status) +{ + int ret, current_uA, capacity; + + ret = ds2780_get_current(dev_info, CURRENT_NOW, ¤t_uA); + if (ret < 0) + return ret; + + ret = ds2780_get_capacity(dev_info, &capacity); + if (ret < 0) + return ret; + + if (capacity == 100) + *status = POWER_SUPPLY_STATUS_FULL; + else if (current_uA == 0) + *status = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (current_uA < 0) + *status = POWER_SUPPLY_STATUS_DISCHARGING; + else + *status = POWER_SUPPLY_STATUS_CHARGING; + + return 0; +} + +static int ds2780_get_charge_now(struct ds2780_device_info *dev_info, + int *charge_now) +{ + int ret; + u16 charge_raw; + + /* + * The RAAC value is located in 16 bits across the RAAC MSB and + * LSB registers + * Bits 15 - 8 of the RAAC value are in bits 7 - 0 of the RAAC + * MSB register + * Bits 7 - 0 of the RAAC value are in bits 7 - 0 of the RAAC + * LSB register + */ + ret = ds2780_read16(dev_info, &charge_raw, DS2780_RAAC_MSB_REG); + if (ret < 0) + return ret; + + *charge_now = charge_raw * 1600; + return 0; +} + +static int ds2780_get_control_register(struct ds2780_device_info *dev_info, + u8 *control_reg) +{ + return ds2780_read8(dev_info, control_reg, DS2780_CONTROL_REG); +} + +static int ds2780_set_control_register(struct ds2780_device_info *dev_info, + u8 control_reg) +{ + int ret; + + ret = ds2780_write(dev_info, &control_reg, + DS2780_CONTROL_REG, sizeof(u8)); + if (ret < 0) + return ret; + + return ds2780_save_eeprom(dev_info, DS2780_CONTROL_REG); +} + +static int ds2780_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = ds2780_get_voltage(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_TEMP: + ret = ds2780_get_temperature(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = model; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = manufacturer; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = ds2780_get_current(dev_info, CURRENT_NOW, &val->intval); + break; + + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = ds2780_get_current(dev_info, CURRENT_AVG, &val->intval); + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = ds2780_get_status(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CAPACITY: + ret = ds2780_get_capacity(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + ret = ds2780_get_accumulated_current(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = ds2780_get_charge_now(dev_info, &val->intval); + break; + + default: + ret = -EINVAL; + } + + return ret; +} + +static enum power_supply_property ds2780_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CHARGE_NOW, +}; + +static ssize_t ds2780_get_pmod_enabled(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 control_reg; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + /* Get power mode */ + ret = ds2780_get_control_register(dev_info, &control_reg); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", + !!(control_reg & DS2780_CONTROL_REG_PMOD)); +} + +static ssize_t ds2780_set_pmod_enabled(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 control_reg, new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + /* Set power mode */ + ret = ds2780_get_control_register(dev_info, &control_reg); + if (ret < 0) + return ret; + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + if ((new_setting != 0) && (new_setting != 1)) { + dev_err(dev_info->dev, "Invalid pmod setting (0 or 1)\n"); + return -EINVAL; + } + + if (new_setting) + control_reg |= DS2780_CONTROL_REG_PMOD; + else + control_reg &= ~DS2780_CONTROL_REG_PMOD; + + ret = ds2780_set_control_register(dev_info, control_reg); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2780_get_sense_resistor_value(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 sense_resistor; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = ds2780_read8(dev_info, &sense_resistor, DS2780_RSNSP_REG); + if (ret < 0) + return ret; + + ret = sprintf(buf, "%d\n", sense_resistor); + return ret; +} + +static ssize_t ds2780_set_sense_resistor_value(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + ret = ds2780_set_sense_register(dev_info, new_setting); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2780_get_rsgain_setting(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u16 rsgain; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = ds2780_get_rsgain_register(dev_info, &rsgain); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", rsgain); +} + +static ssize_t ds2780_set_rsgain_setting(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u16 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = kstrtou16(buf, 0, &new_setting); + if (ret < 0) + return ret; + + /* Gain can only be from 0 to 1.999 in steps of .001 */ + if (new_setting > 1999) { + dev_err(dev_info->dev, "Invalid rsgain setting (0 - 1999)\n"); + return -EINVAL; + } + + ret = ds2780_set_rsgain_register(dev_info, new_setting); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2780_get_pio_pin(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 sfr; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = ds2780_read8(dev_info, &sfr, DS2780_SFR_REG); + if (ret < 0) + return ret; + + ret = sprintf(buf, "%d\n", sfr & DS2780_SFR_REG_PIOSC); + return ret; +} + +static ssize_t ds2780_set_pio_pin(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + if ((new_setting != 0) && (new_setting != 1)) { + dev_err(dev_info->dev, "Invalid pio_pin setting (0 or 1)\n"); + return -EINVAL; + } + + ret = ds2780_write(dev_info, &new_setting, + DS2780_SFR_REG, sizeof(u8)); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2780_read_param_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + return ds2780_read_block(dev_info, buf, + DS2780_EEPROM_BLOCK1_START + off, count); +} + +static ssize_t ds2780_write_param_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + int ret; + + ret = ds2780_write(dev_info, buf, + DS2780_EEPROM_BLOCK1_START + off, count); + if (ret < 0) + return ret; + + ret = ds2780_save_eeprom(dev_info, DS2780_EEPROM_BLOCK1_START); + if (ret < 0) + return ret; + + return count; +} + +static struct bin_attribute ds2780_param_eeprom_bin_attr = { + .attr = { + .name = "param_eeprom", + .mode = S_IRUGO | S_IWUSR, + }, + .size = DS2780_PARAM_EEPROM_SIZE, + .read = ds2780_read_param_eeprom_bin, + .write = ds2780_write_param_eeprom_bin, +}; + +static ssize_t ds2780_read_user_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + + return ds2780_read_block(dev_info, buf, + DS2780_EEPROM_BLOCK0_START + off, count); +} + +static ssize_t ds2780_write_user_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2780_device_info *dev_info = to_ds2780_device_info(psy); + int ret; + + ret = ds2780_write(dev_info, buf, + DS2780_EEPROM_BLOCK0_START + off, count); + if (ret < 0) + return ret; + + ret = ds2780_save_eeprom(dev_info, DS2780_EEPROM_BLOCK0_START); + if (ret < 0) + return ret; + + return count; +} + +static struct bin_attribute ds2780_user_eeprom_bin_attr = { + .attr = { + .name = "user_eeprom", + .mode = S_IRUGO | S_IWUSR, + }, + .size = DS2780_USER_EEPROM_SIZE, + .read = ds2780_read_user_eeprom_bin, + .write = ds2780_write_user_eeprom_bin, +}; + +static DEVICE_ATTR(pmod_enabled, S_IRUGO | S_IWUSR, ds2780_get_pmod_enabled, + ds2780_set_pmod_enabled); +static DEVICE_ATTR(sense_resistor_value, S_IRUGO | S_IWUSR, + ds2780_get_sense_resistor_value, ds2780_set_sense_resistor_value); +static DEVICE_ATTR(rsgain_setting, S_IRUGO | S_IWUSR, ds2780_get_rsgain_setting, + ds2780_set_rsgain_setting); +static DEVICE_ATTR(pio_pin, S_IRUGO | S_IWUSR, ds2780_get_pio_pin, + ds2780_set_pio_pin); + +static struct attribute *ds2780_sysfs_attrs[] = { + &dev_attr_pmod_enabled.attr, + &dev_attr_sense_resistor_value.attr, + &dev_attr_rsgain_setting.attr, + &dev_attr_pio_pin.attr, + NULL +}; + +static struct bin_attribute *ds2780_sysfs_bin_attrs[] = { + &ds2780_param_eeprom_bin_attr, + &ds2780_user_eeprom_bin_attr, + NULL +}; + +static const struct attribute_group ds2780_sysfs_group = { + .attrs = ds2780_sysfs_attrs, + .bin_attrs = ds2780_sysfs_bin_attrs, +}; + +static const struct attribute_group *ds2780_sysfs_groups[] = { + &ds2780_sysfs_group, + NULL, +}; + +static int ds2780_battery_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + struct ds2780_device_info *dev_info; + + dev_info = devm_kzalloc(&pdev->dev, sizeof(*dev_info), GFP_KERNEL); + if (!dev_info) + return -ENOMEM; + + platform_set_drvdata(pdev, dev_info); + + dev_info->dev = &pdev->dev; + dev_info->w1_dev = pdev->dev.parent; + dev_info->bat_desc.name = dev_name(&pdev->dev); + dev_info->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY; + dev_info->bat_desc.properties = ds2780_battery_props; + dev_info->bat_desc.num_properties = ARRAY_SIZE(ds2780_battery_props); + dev_info->bat_desc.get_property = ds2780_battery_get_property; + + psy_cfg.drv_data = dev_info; + psy_cfg.attr_grp = ds2780_sysfs_groups; + + dev_info->bat = devm_power_supply_register(&pdev->dev, + &dev_info->bat_desc, + &psy_cfg); + if (IS_ERR(dev_info->bat)) { + dev_err(dev_info->dev, "failed to register battery\n"); + return PTR_ERR(dev_info->bat); + } + + return 0; +} + +static struct platform_driver ds2780_battery_driver = { + .driver = { + .name = "ds2780-battery", + }, + .probe = ds2780_battery_probe, +}; + +module_platform_driver(ds2780_battery_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Clifton Barnes <cabarnes@indesign-llc.com>"); +MODULE_DESCRIPTION("Maxim/Dallas DS2780 Stand-Alone Fuel Gauge IC driver"); +MODULE_ALIAS("platform:ds2780-battery"); diff --git a/drivers/power/supply/ds2781_battery.c b/drivers/power/supply/ds2781_battery.c new file mode 100644 index 000000000..3df3c820b --- /dev/null +++ b/drivers/power/supply/ds2781_battery.c @@ -0,0 +1,794 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * 1-wire client/driver for the Maxim/Dallas DS2781 Stand-Alone Fuel Gauge IC + * + * Author: Renata Sayakhova <renata@oktetlabs.ru> + * + * Based on ds2780_battery drivers + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/param.h> +#include <linux/pm.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/idr.h> + +#include <linux/w1.h> +#include "../../w1/slaves/w1_ds2781.h" + +/* Current unit measurement in uA for a 1 milli-ohm sense resistor */ +#define DS2781_CURRENT_UNITS 1563 +/* Charge unit measurement in uAh for a 1 milli-ohm sense resistor */ +#define DS2781_CHARGE_UNITS 6250 +/* Number of bytes in user EEPROM space */ +#define DS2781_USER_EEPROM_SIZE (DS2781_EEPROM_BLOCK0_END - \ + DS2781_EEPROM_BLOCK0_START + 1) +/* Number of bytes in parameter EEPROM space */ +#define DS2781_PARAM_EEPROM_SIZE (DS2781_EEPROM_BLOCK1_END - \ + DS2781_EEPROM_BLOCK1_START + 1) + +struct ds2781_device_info { + struct device *dev; + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct device *w1_dev; +}; + +enum current_types { + CURRENT_NOW, + CURRENT_AVG, +}; + +static const char model[] = "DS2781"; +static const char manufacturer[] = "Maxim/Dallas"; + +static inline struct ds2781_device_info * +to_ds2781_device_info(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static inline int ds2781_battery_io(struct ds2781_device_info *dev_info, + char *buf, int addr, size_t count, int io) +{ + return w1_ds2781_io(dev_info->w1_dev, buf, addr, count, io); +} + +static int w1_ds2781_read(struct ds2781_device_info *dev_info, char *buf, + int addr, size_t count) +{ + return ds2781_battery_io(dev_info, buf, addr, count, 0); +} + +static inline int ds2781_read8(struct ds2781_device_info *dev_info, u8 *val, + int addr) +{ + return ds2781_battery_io(dev_info, val, addr, sizeof(u8), 0); +} + +static int ds2781_read16(struct ds2781_device_info *dev_info, s16 *val, + int addr) +{ + int ret; + u8 raw[2]; + + ret = ds2781_battery_io(dev_info, raw, addr, sizeof(raw), 0); + if (ret < 0) + return ret; + + *val = (raw[0] << 8) | raw[1]; + + return 0; +} + +static inline int ds2781_read_block(struct ds2781_device_info *dev_info, + u8 *val, int addr, size_t count) +{ + return ds2781_battery_io(dev_info, val, addr, count, 0); +} + +static inline int ds2781_write(struct ds2781_device_info *dev_info, u8 *val, + int addr, size_t count) +{ + return ds2781_battery_io(dev_info, val, addr, count, 1); +} + +static inline int ds2781_store_eeprom(struct device *dev, int addr) +{ + return w1_ds2781_eeprom_cmd(dev, addr, W1_DS2781_COPY_DATA); +} + +static inline int ds2781_recall_eeprom(struct device *dev, int addr) +{ + return w1_ds2781_eeprom_cmd(dev, addr, W1_DS2781_RECALL_DATA); +} + +static int ds2781_save_eeprom(struct ds2781_device_info *dev_info, int reg) +{ + int ret; + + ret = ds2781_store_eeprom(dev_info->w1_dev, reg); + if (ret < 0) + return ret; + + ret = ds2781_recall_eeprom(dev_info->w1_dev, reg); + if (ret < 0) + return ret; + + return 0; +} + +/* Set sense resistor value in mhos */ +static int ds2781_set_sense_register(struct ds2781_device_info *dev_info, + u8 conductance) +{ + int ret; + + ret = ds2781_write(dev_info, &conductance, + DS2781_RSNSP, sizeof(u8)); + if (ret < 0) + return ret; + + return ds2781_save_eeprom(dev_info, DS2781_RSNSP); +} + +/* Get RSGAIN value from 0 to 1.999 in steps of 0.001 */ +static int ds2781_get_rsgain_register(struct ds2781_device_info *dev_info, + u16 *rsgain) +{ + return ds2781_read16(dev_info, rsgain, DS2781_RSGAIN_MSB); +} + +/* Set RSGAIN value from 0 to 1.999 in steps of 0.001 */ +static int ds2781_set_rsgain_register(struct ds2781_device_info *dev_info, + u16 rsgain) +{ + int ret; + u8 raw[] = {rsgain >> 8, rsgain & 0xFF}; + + ret = ds2781_write(dev_info, raw, + DS2781_RSGAIN_MSB, sizeof(raw)); + if (ret < 0) + return ret; + + return ds2781_save_eeprom(dev_info, DS2781_RSGAIN_MSB); +} + +static int ds2781_get_voltage(struct ds2781_device_info *dev_info, + int *voltage_uV) +{ + int ret; + char val[2]; + int voltage_raw; + + ret = w1_ds2781_read(dev_info, val, DS2781_VOLT_MSB, 2 * sizeof(u8)); + if (ret < 0) + return ret; + /* + * The voltage value is located in 10 bits across the voltage MSB + * and LSB registers in two's complement form + * Sign bit of the voltage value is in bit 7 of the voltage MSB register + * Bits 9 - 3 of the voltage value are in bits 6 - 0 of the + * voltage MSB register + * Bits 2 - 0 of the voltage value are in bits 7 - 5 of the + * voltage LSB register + */ + voltage_raw = (val[0] << 3) | + (val[1] >> 5); + + /* DS2781 reports voltage in units of 9.76mV, but the battery class + * reports in units of uV, so convert by multiplying by 9760. */ + *voltage_uV = voltage_raw * 9760; + + return 0; +} + +static int ds2781_get_temperature(struct ds2781_device_info *dev_info, + int *temp) +{ + int ret; + char val[2]; + int temp_raw; + + ret = w1_ds2781_read(dev_info, val, DS2781_TEMP_MSB, 2 * sizeof(u8)); + if (ret < 0) + return ret; + /* + * The temperature value is located in 10 bits across the temperature + * MSB and LSB registers in two's complement form + * Sign bit of the temperature value is in bit 7 of the temperature + * MSB register + * Bits 9 - 3 of the temperature value are in bits 6 - 0 of the + * temperature MSB register + * Bits 2 - 0 of the temperature value are in bits 7 - 5 of the + * temperature LSB register + */ + temp_raw = ((val[0]) << 3) | + (val[1] >> 5); + *temp = temp_raw + (temp_raw / 4); + + return 0; +} + +static int ds2781_get_current(struct ds2781_device_info *dev_info, + enum current_types type, int *current_uA) +{ + int ret, sense_res; + s16 current_raw; + u8 sense_res_raw, reg_msb; + + /* + * The units of measurement for current are dependent on the value of + * the sense resistor. + */ + ret = ds2781_read8(dev_info, &sense_res_raw, DS2781_RSNSP); + if (ret < 0) + return ret; + + if (sense_res_raw == 0) { + dev_err(dev_info->dev, "sense resistor value is 0\n"); + return -EINVAL; + } + sense_res = 1000 / sense_res_raw; + + if (type == CURRENT_NOW) + reg_msb = DS2781_CURRENT_MSB; + else if (type == CURRENT_AVG) + reg_msb = DS2781_IAVG_MSB; + else + return -EINVAL; + + /* + * The current value is located in 16 bits across the current MSB + * and LSB registers in two's complement form + * Sign bit of the current value is in bit 7 of the current MSB register + * Bits 14 - 8 of the current value are in bits 6 - 0 of the current + * MSB register + * Bits 7 - 0 of the current value are in bits 7 - 0 of the current + * LSB register + */ + ret = ds2781_read16(dev_info, ¤t_raw, reg_msb); + if (ret < 0) + return ret; + + *current_uA = current_raw * (DS2781_CURRENT_UNITS / sense_res); + return 0; +} + +static int ds2781_get_accumulated_current(struct ds2781_device_info *dev_info, + int *accumulated_current) +{ + int ret, sense_res; + s16 current_raw; + u8 sense_res_raw; + + /* + * The units of measurement for accumulated current are dependent on + * the value of the sense resistor. + */ + ret = ds2781_read8(dev_info, &sense_res_raw, DS2781_RSNSP); + if (ret < 0) + return ret; + + if (sense_res_raw == 0) { + dev_err(dev_info->dev, "sense resistor value is 0\n"); + return -EINVAL; + } + sense_res = 1000 / sense_res_raw; + + /* + * The ACR value is located in 16 bits across the ACR MSB and + * LSB registers + * Bits 15 - 8 of the ACR value are in bits 7 - 0 of the ACR + * MSB register + * Bits 7 - 0 of the ACR value are in bits 7 - 0 of the ACR + * LSB register + */ + ret = ds2781_read16(dev_info, ¤t_raw, DS2781_ACR_MSB); + if (ret < 0) + return ret; + + *accumulated_current = current_raw * (DS2781_CHARGE_UNITS / sense_res); + return 0; +} + +static int ds2781_get_capacity(struct ds2781_device_info *dev_info, + int *capacity) +{ + int ret; + u8 raw; + + ret = ds2781_read8(dev_info, &raw, DS2781_RARC); + if (ret < 0) + return ret; + + *capacity = raw; + return 0; +} + +static int ds2781_get_status(struct ds2781_device_info *dev_info, int *status) +{ + int ret, current_uA, capacity; + + ret = ds2781_get_current(dev_info, CURRENT_NOW, ¤t_uA); + if (ret < 0) + return ret; + + ret = ds2781_get_capacity(dev_info, &capacity); + if (ret < 0) + return ret; + + if (power_supply_am_i_supplied(dev_info->bat)) { + if (capacity == 100) + *status = POWER_SUPPLY_STATUS_FULL; + else if (current_uA > 50000) + *status = POWER_SUPPLY_STATUS_CHARGING; + else + *status = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + *status = POWER_SUPPLY_STATUS_DISCHARGING; + } + return 0; +} + +static int ds2781_get_charge_now(struct ds2781_device_info *dev_info, + int *charge_now) +{ + int ret; + u16 charge_raw; + + /* + * The RAAC value is located in 16 bits across the RAAC MSB and + * LSB registers + * Bits 15 - 8 of the RAAC value are in bits 7 - 0 of the RAAC + * MSB register + * Bits 7 - 0 of the RAAC value are in bits 7 - 0 of the RAAC + * LSB register + */ + ret = ds2781_read16(dev_info, &charge_raw, DS2781_RAAC_MSB); + if (ret < 0) + return ret; + + *charge_now = charge_raw * 1600; + return 0; +} + +static int ds2781_get_control_register(struct ds2781_device_info *dev_info, + u8 *control_reg) +{ + return ds2781_read8(dev_info, control_reg, DS2781_CONTROL); +} + +static int ds2781_set_control_register(struct ds2781_device_info *dev_info, + u8 control_reg) +{ + int ret; + + ret = ds2781_write(dev_info, &control_reg, + DS2781_CONTROL, sizeof(u8)); + if (ret < 0) + return ret; + + return ds2781_save_eeprom(dev_info, DS2781_CONTROL); +} + +static int ds2781_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = ds2781_get_voltage(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_TEMP: + ret = ds2781_get_temperature(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = model; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = manufacturer; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = ds2781_get_current(dev_info, CURRENT_NOW, &val->intval); + break; + + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = ds2781_get_current(dev_info, CURRENT_AVG, &val->intval); + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = ds2781_get_status(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CAPACITY: + ret = ds2781_get_capacity(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + ret = ds2781_get_accumulated_current(dev_info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = ds2781_get_charge_now(dev_info, &val->intval); + break; + + default: + ret = -EINVAL; + } + + return ret; +} + +static enum power_supply_property ds2781_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CHARGE_NOW, +}; + +static ssize_t ds2781_get_pmod_enabled(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 control_reg; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + /* Get power mode */ + ret = ds2781_get_control_register(dev_info, &control_reg); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", + !!(control_reg & DS2781_CONTROL_PMOD)); +} + +static ssize_t ds2781_set_pmod_enabled(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 control_reg, new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + /* Set power mode */ + ret = ds2781_get_control_register(dev_info, &control_reg); + if (ret < 0) + return ret; + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + if ((new_setting != 0) && (new_setting != 1)) { + dev_err(dev_info->dev, "Invalid pmod setting (0 or 1)\n"); + return -EINVAL; + } + + if (new_setting) + control_reg |= DS2781_CONTROL_PMOD; + else + control_reg &= ~DS2781_CONTROL_PMOD; + + ret = ds2781_set_control_register(dev_info, control_reg); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2781_get_sense_resistor_value(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 sense_resistor; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = ds2781_read8(dev_info, &sense_resistor, DS2781_RSNSP); + if (ret < 0) + return ret; + + ret = sprintf(buf, "%d\n", sense_resistor); + return ret; +} + +static ssize_t ds2781_set_sense_resistor_value(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + ret = ds2781_set_sense_register(dev_info, new_setting); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2781_get_rsgain_setting(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u16 rsgain; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = ds2781_get_rsgain_register(dev_info, &rsgain); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", rsgain); +} + +static ssize_t ds2781_set_rsgain_setting(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u16 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = kstrtou16(buf, 0, &new_setting); + if (ret < 0) + return ret; + + /* Gain can only be from 0 to 1.999 in steps of .001 */ + if (new_setting > 1999) { + dev_err(dev_info->dev, "Invalid rsgain setting (0 - 1999)\n"); + return -EINVAL; + } + + ret = ds2781_set_rsgain_register(dev_info, new_setting); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2781_get_pio_pin(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int ret; + u8 sfr; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = ds2781_read8(dev_info, &sfr, DS2781_SFR); + if (ret < 0) + return ret; + + ret = sprintf(buf, "%d\n", sfr & DS2781_SFR_PIOSC); + return ret; +} + +static ssize_t ds2781_set_pio_pin(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + int ret; + u8 new_setting; + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + ret = kstrtou8(buf, 0, &new_setting); + if (ret < 0) + return ret; + + if ((new_setting != 0) && (new_setting != 1)) { + dev_err(dev_info->dev, "Invalid pio_pin setting (0 or 1)\n"); + return -EINVAL; + } + + ret = ds2781_write(dev_info, &new_setting, + DS2781_SFR, sizeof(u8)); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t ds2781_read_param_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + return ds2781_read_block(dev_info, buf, + DS2781_EEPROM_BLOCK1_START + off, count); +} + +static ssize_t ds2781_write_param_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + int ret; + + ret = ds2781_write(dev_info, buf, + DS2781_EEPROM_BLOCK1_START + off, count); + if (ret < 0) + return ret; + + ret = ds2781_save_eeprom(dev_info, DS2781_EEPROM_BLOCK1_START); + if (ret < 0) + return ret; + + return count; +} + +static struct bin_attribute ds2781_param_eeprom_bin_attr = { + .attr = { + .name = "param_eeprom", + .mode = S_IRUGO | S_IWUSR, + }, + .size = DS2781_PARAM_EEPROM_SIZE, + .read = ds2781_read_param_eeprom_bin, + .write = ds2781_write_param_eeprom_bin, +}; + +static ssize_t ds2781_read_user_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + + return ds2781_read_block(dev_info, buf, + DS2781_EEPROM_BLOCK0_START + off, count); + +} + +static ssize_t ds2781_write_user_eeprom_bin(struct file *filp, + struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct power_supply *psy = to_power_supply(dev); + struct ds2781_device_info *dev_info = to_ds2781_device_info(psy); + int ret; + + ret = ds2781_write(dev_info, buf, + DS2781_EEPROM_BLOCK0_START + off, count); + if (ret < 0) + return ret; + + ret = ds2781_save_eeprom(dev_info, DS2781_EEPROM_BLOCK0_START); + if (ret < 0) + return ret; + + return count; +} + +static struct bin_attribute ds2781_user_eeprom_bin_attr = { + .attr = { + .name = "user_eeprom", + .mode = S_IRUGO | S_IWUSR, + }, + .size = DS2781_USER_EEPROM_SIZE, + .read = ds2781_read_user_eeprom_bin, + .write = ds2781_write_user_eeprom_bin, +}; + +static DEVICE_ATTR(pmod_enabled, S_IRUGO | S_IWUSR, ds2781_get_pmod_enabled, + ds2781_set_pmod_enabled); +static DEVICE_ATTR(sense_resistor_value, S_IRUGO | S_IWUSR, + ds2781_get_sense_resistor_value, ds2781_set_sense_resistor_value); +static DEVICE_ATTR(rsgain_setting, S_IRUGO | S_IWUSR, ds2781_get_rsgain_setting, + ds2781_set_rsgain_setting); +static DEVICE_ATTR(pio_pin, S_IRUGO | S_IWUSR, ds2781_get_pio_pin, + ds2781_set_pio_pin); + +static struct attribute *ds2781_sysfs_attrs[] = { + &dev_attr_pmod_enabled.attr, + &dev_attr_sense_resistor_value.attr, + &dev_attr_rsgain_setting.attr, + &dev_attr_pio_pin.attr, + NULL +}; + +static struct bin_attribute *ds2781_sysfs_bin_attrs[] = { + &ds2781_param_eeprom_bin_attr, + &ds2781_user_eeprom_bin_attr, + NULL, +}; + +static const struct attribute_group ds2781_sysfs_group = { + .attrs = ds2781_sysfs_attrs, + .bin_attrs = ds2781_sysfs_bin_attrs, + +}; + +static const struct attribute_group *ds2781_sysfs_groups[] = { + &ds2781_sysfs_group, + NULL, +}; + +static int ds2781_battery_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + struct ds2781_device_info *dev_info; + + dev_info = devm_kzalloc(&pdev->dev, sizeof(*dev_info), GFP_KERNEL); + if (!dev_info) + return -ENOMEM; + + platform_set_drvdata(pdev, dev_info); + + dev_info->dev = &pdev->dev; + dev_info->w1_dev = pdev->dev.parent; + dev_info->bat_desc.name = dev_name(&pdev->dev); + dev_info->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY; + dev_info->bat_desc.properties = ds2781_battery_props; + dev_info->bat_desc.num_properties = ARRAY_SIZE(ds2781_battery_props); + dev_info->bat_desc.get_property = ds2781_battery_get_property; + + psy_cfg.drv_data = dev_info; + psy_cfg.attr_grp = ds2781_sysfs_groups; + + dev_info->bat = devm_power_supply_register(&pdev->dev, + &dev_info->bat_desc, + &psy_cfg); + if (IS_ERR(dev_info->bat)) { + dev_err(dev_info->dev, "failed to register battery\n"); + return PTR_ERR(dev_info->bat); + } + + return 0; +} + +static struct platform_driver ds2781_battery_driver = { + .driver = { + .name = "ds2781-battery", + }, + .probe = ds2781_battery_probe, +}; +module_platform_driver(ds2781_battery_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Renata Sayakhova <renata@oktetlabs.ru>"); +MODULE_DESCRIPTION("Maxim/Dallas DS2781 Stand-Alone Fuel Gauge IC driver"); +MODULE_ALIAS("platform:ds2781-battery"); + diff --git a/drivers/power/supply/ds2782_battery.c b/drivers/power/supply/ds2782_battery.c new file mode 100644 index 000000000..9ae273fde --- /dev/null +++ b/drivers/power/supply/ds2782_battery.c @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * I2C client/driver for the Maxim/Dallas DS2782 Stand-Alone Fuel Gauge IC + * + * Copyright (C) 2009 Bluewater Systems Ltd + * + * Author: Ryan Mallon + * + * DS2786 added by Yulia Vilensky <vilensky@compulab.co.il> + * + * UEvent sending added by Evgeny Romanov <romanov@neurosoft.ru> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/types.h> +#include <linux/errno.h> +#include <linux/swab.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include <linux/idr.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/ds2782_battery.h> + +#define DS2782_REG_RARC 0x06 /* Remaining active relative capacity */ + +#define DS278x_REG_VOLT_MSB 0x0c +#define DS278x_REG_TEMP_MSB 0x0a +#define DS278x_REG_CURRENT_MSB 0x0e + +/* EEPROM Block */ +#define DS2782_REG_RSNSP 0x69 /* Sense resistor value */ + +/* Current unit measurement in uA for a 1 milli-ohm sense resistor */ +#define DS2782_CURRENT_UNITS 1563 + +#define DS2786_REG_RARC 0x02 /* Remaining active relative capacity */ + +#define DS2786_CURRENT_UNITS 25 + +#define DS278x_DELAY 1000 + +struct ds278x_info; + +struct ds278x_battery_ops { + int (*get_battery_current)(struct ds278x_info *info, int *current_uA); + int (*get_battery_voltage)(struct ds278x_info *info, int *voltage_uV); + int (*get_battery_capacity)(struct ds278x_info *info, int *capacity); +}; + +#define to_ds278x_info(x) power_supply_get_drvdata(x) + +struct ds278x_info { + struct i2c_client *client; + struct power_supply *battery; + struct power_supply_desc battery_desc; + const struct ds278x_battery_ops *ops; + struct delayed_work bat_work; + int id; + int rsns; + int capacity; + int status; /* State Of Charge */ +}; + +static DEFINE_IDR(battery_id); +static DEFINE_MUTEX(battery_lock); + +static inline int ds278x_read_reg(struct ds278x_info *info, int reg, u8 *val) +{ + int ret; + + ret = i2c_smbus_read_byte_data(info->client, reg); + if (ret < 0) { + dev_err(&info->client->dev, "register read failed\n"); + return ret; + } + + *val = ret; + return 0; +} + +static inline int ds278x_read_reg16(struct ds278x_info *info, int reg_msb, + s16 *val) +{ + int ret; + + ret = i2c_smbus_read_word_data(info->client, reg_msb); + if (ret < 0) { + dev_err(&info->client->dev, "register read failed\n"); + return ret; + } + + *val = swab16(ret); + return 0; +} + +static int ds278x_get_temp(struct ds278x_info *info, int *temp) +{ + s16 raw; + int err; + + /* + * Temperature is measured in units of 0.125 degrees celcius, the + * power_supply class measures temperature in tenths of degrees + * celsius. The temperature value is stored as a 10 bit number, plus + * sign in the upper bits of a 16 bit register. + */ + err = ds278x_read_reg16(info, DS278x_REG_TEMP_MSB, &raw); + if (err) + return err; + *temp = ((raw / 32) * 125) / 100; + return 0; +} + +static int ds2782_get_current(struct ds278x_info *info, int *current_uA) +{ + int sense_res; + int err; + u8 sense_res_raw; + s16 raw; + + /* + * The units of measurement for current are dependent on the value of + * the sense resistor. + */ + err = ds278x_read_reg(info, DS2782_REG_RSNSP, &sense_res_raw); + if (err) + return err; + if (sense_res_raw == 0) { + dev_err(&info->client->dev, "sense resistor value is 0\n"); + return -ENXIO; + } + sense_res = 1000 / sense_res_raw; + + dev_dbg(&info->client->dev, "sense resistor = %d milli-ohms\n", + sense_res); + err = ds278x_read_reg16(info, DS278x_REG_CURRENT_MSB, &raw); + if (err) + return err; + *current_uA = raw * (DS2782_CURRENT_UNITS / sense_res); + return 0; +} + +static int ds2782_get_voltage(struct ds278x_info *info, int *voltage_uV) +{ + s16 raw; + int err; + + /* + * Voltage is measured in units of 4.88mV. The voltage is stored as + * a 10-bit number plus sign, in the upper bits of a 16-bit register + */ + err = ds278x_read_reg16(info, DS278x_REG_VOLT_MSB, &raw); + if (err) + return err; + *voltage_uV = (raw / 32) * 4800; + return 0; +} + +static int ds2782_get_capacity(struct ds278x_info *info, int *capacity) +{ + int err; + u8 raw; + + err = ds278x_read_reg(info, DS2782_REG_RARC, &raw); + if (err) + return err; + *capacity = raw; + return 0; +} + +static int ds2786_get_current(struct ds278x_info *info, int *current_uA) +{ + int err; + s16 raw; + + err = ds278x_read_reg16(info, DS278x_REG_CURRENT_MSB, &raw); + if (err) + return err; + *current_uA = (raw / 16) * (DS2786_CURRENT_UNITS / info->rsns); + return 0; +} + +static int ds2786_get_voltage(struct ds278x_info *info, int *voltage_uV) +{ + s16 raw; + int err; + + /* + * Voltage is measured in units of 1.22mV. The voltage is stored as + * a 12-bit number plus sign, in the upper bits of a 16-bit register + */ + err = ds278x_read_reg16(info, DS278x_REG_VOLT_MSB, &raw); + if (err) + return err; + *voltage_uV = (raw / 8) * 1220; + return 0; +} + +static int ds2786_get_capacity(struct ds278x_info *info, int *capacity) +{ + int err; + u8 raw; + + err = ds278x_read_reg(info, DS2786_REG_RARC, &raw); + if (err) + return err; + /* Relative capacity is displayed with resolution 0.5 % */ + *capacity = raw/2 ; + return 0; +} + +static int ds278x_get_status(struct ds278x_info *info, int *status) +{ + int err; + int current_uA; + int capacity; + + err = info->ops->get_battery_current(info, ¤t_uA); + if (err) + return err; + + err = info->ops->get_battery_capacity(info, &capacity); + if (err) + return err; + + info->capacity = capacity; + + if (capacity == 100) + *status = POWER_SUPPLY_STATUS_FULL; + else if (current_uA == 0) + *status = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (current_uA < 0) + *status = POWER_SUPPLY_STATUS_DISCHARGING; + else + *status = POWER_SUPPLY_STATUS_CHARGING; + + return 0; +} + +static int ds278x_battery_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct ds278x_info *info = to_ds278x_info(psy); + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_STATUS: + ret = ds278x_get_status(info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CAPACITY: + ret = info->ops->get_battery_capacity(info, &val->intval); + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = info->ops->get_battery_voltage(info, &val->intval); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = info->ops->get_battery_current(info, &val->intval); + break; + + case POWER_SUPPLY_PROP_TEMP: + ret = ds278x_get_temp(info, &val->intval); + break; + + default: + ret = -EINVAL; + } + + return ret; +} + +static void ds278x_bat_update(struct ds278x_info *info) +{ + int old_status = info->status; + int old_capacity = info->capacity; + + ds278x_get_status(info, &info->status); + + if ((old_status != info->status) || (old_capacity != info->capacity)) + power_supply_changed(info->battery); +} + +static void ds278x_bat_work(struct work_struct *work) +{ + struct ds278x_info *info; + + info = container_of(work, struct ds278x_info, bat_work.work); + ds278x_bat_update(info); + + schedule_delayed_work(&info->bat_work, DS278x_DELAY); +} + +static enum power_supply_property ds278x_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_TEMP, +}; + +static void ds278x_power_supply_init(struct power_supply_desc *battery) +{ + battery->type = POWER_SUPPLY_TYPE_BATTERY; + battery->properties = ds278x_battery_props; + battery->num_properties = ARRAY_SIZE(ds278x_battery_props); + battery->get_property = ds278x_battery_get_property; + battery->external_power_changed = NULL; +} + +static int ds278x_battery_remove(struct i2c_client *client) +{ + struct ds278x_info *info = i2c_get_clientdata(client); + int id = info->id; + + power_supply_unregister(info->battery); + cancel_delayed_work_sync(&info->bat_work); + kfree(info->battery_desc.name); + kfree(info); + + mutex_lock(&battery_lock); + idr_remove(&battery_id, id); + mutex_unlock(&battery_lock); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP + +static int ds278x_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct ds278x_info *info = i2c_get_clientdata(client); + + cancel_delayed_work(&info->bat_work); + return 0; +} + +static int ds278x_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct ds278x_info *info = i2c_get_clientdata(client); + + schedule_delayed_work(&info->bat_work, DS278x_DELAY); + return 0; +} +#endif /* CONFIG_PM_SLEEP */ + +static SIMPLE_DEV_PM_OPS(ds278x_battery_pm_ops, ds278x_suspend, ds278x_resume); + +enum ds278x_num_id { + DS2782 = 0, + DS2786, +}; + +static const struct ds278x_battery_ops ds278x_ops[] = { + [DS2782] = { + .get_battery_current = ds2782_get_current, + .get_battery_voltage = ds2782_get_voltage, + .get_battery_capacity = ds2782_get_capacity, + }, + [DS2786] = { + .get_battery_current = ds2786_get_current, + .get_battery_voltage = ds2786_get_voltage, + .get_battery_capacity = ds2786_get_capacity, + } +}; + +static int ds278x_battery_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct ds278x_platform_data *pdata = client->dev.platform_data; + struct power_supply_config psy_cfg = {}; + struct ds278x_info *info; + int ret; + int num; + + /* + * ds2786 should have the sense resistor value set + * in the platform data + */ + if (id->driver_data == DS2786 && !pdata) { + dev_err(&client->dev, "missing platform data for ds2786\n"); + return -EINVAL; + } + + /* Get an ID for this battery */ + mutex_lock(&battery_lock); + ret = idr_alloc(&battery_id, client, 0, 0, GFP_KERNEL); + mutex_unlock(&battery_lock); + if (ret < 0) + goto fail_id; + num = ret; + + info = kzalloc(sizeof(*info), GFP_KERNEL); + if (!info) { + ret = -ENOMEM; + goto fail_info; + } + + info->battery_desc.name = kasprintf(GFP_KERNEL, "%s-%d", + client->name, num); + if (!info->battery_desc.name) { + ret = -ENOMEM; + goto fail_name; + } + + if (id->driver_data == DS2786) + info->rsns = pdata->rsns; + + i2c_set_clientdata(client, info); + info->client = client; + info->id = num; + info->ops = &ds278x_ops[id->driver_data]; + ds278x_power_supply_init(&info->battery_desc); + psy_cfg.drv_data = info; + + info->capacity = 100; + info->status = POWER_SUPPLY_STATUS_FULL; + + INIT_DELAYED_WORK(&info->bat_work, ds278x_bat_work); + + info->battery = power_supply_register(&client->dev, + &info->battery_desc, &psy_cfg); + if (IS_ERR(info->battery)) { + dev_err(&client->dev, "failed to register battery\n"); + ret = PTR_ERR(info->battery); + goto fail_register; + } else { + schedule_delayed_work(&info->bat_work, DS278x_DELAY); + } + + return 0; + +fail_register: + kfree(info->battery_desc.name); +fail_name: + kfree(info); +fail_info: + mutex_lock(&battery_lock); + idr_remove(&battery_id, num); + mutex_unlock(&battery_lock); +fail_id: + return ret; +} + +static const struct i2c_device_id ds278x_id[] = { + {"ds2782", DS2782}, + {"ds2786", DS2786}, + {}, +}; +MODULE_DEVICE_TABLE(i2c, ds278x_id); + +static struct i2c_driver ds278x_battery_driver = { + .driver = { + .name = "ds2782-battery", + .pm = &ds278x_battery_pm_ops, + }, + .probe = ds278x_battery_probe, + .remove = ds278x_battery_remove, + .id_table = ds278x_id, +}; +module_i2c_driver(ds278x_battery_driver); + +MODULE_AUTHOR("Ryan Mallon"); +MODULE_DESCRIPTION("Maxim/Dallas DS2782 Stand-Alone Fuel Gauge IC driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/generic-adc-battery.c b/drivers/power/supply/generic-adc-battery.c new file mode 100644 index 000000000..09bb4ff29 --- /dev/null +++ b/drivers/power/supply/generic-adc-battery.c @@ -0,0 +1,429 @@ +/* + * Generic battery driver code using IIO + * Copyright (C) 2012, Anish Kumar <anish198519851985@gmail.com> + * based on jz4740-battery.c + * based on s3c_adc_battery.c + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of this archive for + * more details. + * + */ +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/gpio.h> +#include <linux/err.h> +#include <linux/timer.h> +#include <linux/jiffies.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/iio/consumer.h> +#include <linux/iio/types.h> +#include <linux/power/generic-adc-battery.h> + +#define JITTER_DEFAULT 10 /* hope 10ms is enough */ + +enum gab_chan_type { + GAB_VOLTAGE = 0, + GAB_CURRENT, + GAB_POWER, + GAB_MAX_CHAN_TYPE +}; + +/* + * gab_chan_name suggests the standard channel names for commonly used + * channel types. + */ +static const char *const gab_chan_name[] = { + [GAB_VOLTAGE] = "voltage", + [GAB_CURRENT] = "current", + [GAB_POWER] = "power", +}; + +struct gab { + struct power_supply *psy; + struct power_supply_desc psy_desc; + struct iio_channel *channel[GAB_MAX_CHAN_TYPE]; + struct gab_platform_data *pdata; + struct delayed_work bat_work; + int level; + int status; + bool cable_plugged; +}; + +static struct gab *to_generic_bat(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static void gab_ext_power_changed(struct power_supply *psy) +{ + struct gab *adc_bat = to_generic_bat(psy); + + schedule_delayed_work(&adc_bat->bat_work, msecs_to_jiffies(0)); +} + +static const enum power_supply_property gab_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +/* + * This properties are set based on the received platform data and this + * should correspond one-to-one with enum chan_type. + */ +static const enum power_supply_property gab_dyn_props[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_POWER_NOW, +}; + +static bool gab_charge_finished(struct gab *adc_bat) +{ + struct gab_platform_data *pdata = adc_bat->pdata; + bool ret = gpio_get_value(pdata->gpio_charge_finished); + bool inv = pdata->gpio_inverted; + + if (!gpio_is_valid(pdata->gpio_charge_finished)) + return false; + return ret ^ inv; +} + +static int gab_get_status(struct gab *adc_bat) +{ + struct gab_platform_data *pdata = adc_bat->pdata; + struct power_supply_info *bat_info; + + bat_info = &pdata->battery_info; + if (adc_bat->level == bat_info->charge_full_design) + return POWER_SUPPLY_STATUS_FULL; + return adc_bat->status; +} + +static enum gab_chan_type gab_prop_to_chan(enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_POWER_NOW: + return GAB_POWER; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return GAB_VOLTAGE; + case POWER_SUPPLY_PROP_CURRENT_NOW: + return GAB_CURRENT; + default: + WARN_ON(1); + break; + } + return GAB_POWER; +} + +static int read_channel(struct gab *adc_bat, enum power_supply_property psp, + int *result) +{ + int ret; + int chan_index; + + chan_index = gab_prop_to_chan(psp); + ret = iio_read_channel_processed(adc_bat->channel[chan_index], + result); + if (ret < 0) + pr_err("read channel error\n"); + else + *result *= 1000; + + return ret; +} + +static int gab_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct gab *adc_bat; + struct gab_platform_data *pdata; + struct power_supply_info *bat_info; + int result = 0; + int ret = 0; + + adc_bat = to_generic_bat(psy); + if (!adc_bat) { + dev_err(&psy->dev, "no battery infos ?!\n"); + return -EINVAL; + } + pdata = adc_bat->pdata; + bat_info = &pdata->battery_info; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = gab_get_status(adc_bat); + break; + case POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN: + val->intval = 0; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = pdata->cal_charge(result); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_CURRENT_NOW: + case POWER_SUPPLY_PROP_POWER_NOW: + ret = read_channel(adc_bat, psp, &result); + if (ret < 0) + goto err; + val->intval = result; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = bat_info->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = bat_info->voltage_min_design; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = bat_info->voltage_max_design; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = bat_info->charge_full_design; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = bat_info->name; + break; + default: + return -EINVAL; + } +err: + return ret; +} + +static void gab_work(struct work_struct *work) +{ + struct gab *adc_bat; + struct delayed_work *delayed_work; + bool is_plugged; + int status; + + delayed_work = to_delayed_work(work); + adc_bat = container_of(delayed_work, struct gab, bat_work); + status = adc_bat->status; + + is_plugged = power_supply_am_i_supplied(adc_bat->psy); + adc_bat->cable_plugged = is_plugged; + + if (!is_plugged) + adc_bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + else if (gab_charge_finished(adc_bat)) + adc_bat->status = POWER_SUPPLY_STATUS_NOT_CHARGING; + else + adc_bat->status = POWER_SUPPLY_STATUS_CHARGING; + + if (status != adc_bat->status) + power_supply_changed(adc_bat->psy); +} + +static irqreturn_t gab_charged(int irq, void *dev_id) +{ + struct gab *adc_bat = dev_id; + struct gab_platform_data *pdata = adc_bat->pdata; + int delay; + + delay = pdata->jitter_delay ? pdata->jitter_delay : JITTER_DEFAULT; + schedule_delayed_work(&adc_bat->bat_work, + msecs_to_jiffies(delay)); + return IRQ_HANDLED; +} + +static int gab_probe(struct platform_device *pdev) +{ + struct gab *adc_bat; + struct power_supply_desc *psy_desc; + struct power_supply_config psy_cfg = {}; + struct gab_platform_data *pdata = pdev->dev.platform_data; + enum power_supply_property *properties; + int ret = 0; + int chan; + int index = ARRAY_SIZE(gab_props); + bool any = false; + + adc_bat = devm_kzalloc(&pdev->dev, sizeof(*adc_bat), GFP_KERNEL); + if (!adc_bat) { + dev_err(&pdev->dev, "failed to allocate memory\n"); + return -ENOMEM; + } + + psy_cfg.drv_data = adc_bat; + psy_desc = &adc_bat->psy_desc; + psy_desc->name = pdata->battery_info.name; + + /* bootup default values for the battery */ + adc_bat->cable_plugged = false; + adc_bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; + psy_desc->get_property = gab_get_property; + psy_desc->external_power_changed = gab_ext_power_changed; + adc_bat->pdata = pdata; + + /* + * copying the static properties and allocating extra memory for holding + * the extra configurable properties received from platform data. + */ + properties = kcalloc(ARRAY_SIZE(gab_props) + + ARRAY_SIZE(gab_chan_name), + sizeof(*properties), + GFP_KERNEL); + if (!properties) { + ret = -ENOMEM; + goto first_mem_fail; + } + + memcpy(properties, gab_props, sizeof(gab_props)); + + /* + * getting channel from iio and copying the battery properties + * based on the channel supported by consumer device. + */ + for (chan = 0; chan < ARRAY_SIZE(gab_chan_name); chan++) { + adc_bat->channel[chan] = iio_channel_get(&pdev->dev, + gab_chan_name[chan]); + if (IS_ERR(adc_bat->channel[chan])) { + ret = PTR_ERR(adc_bat->channel[chan]); + adc_bat->channel[chan] = NULL; + } else { + /* copying properties for supported channels only */ + int index2; + + for (index2 = 0; index2 < index; index2++) { + if (properties[index2] == gab_dyn_props[chan]) + break; /* already known */ + } + if (index2 == index) /* really new */ + properties[index++] = gab_dyn_props[chan]; + any = true; + } + } + + /* none of the channels are supported so let's bail out */ + if (!any) { + ret = -ENODEV; + goto second_mem_fail; + } + + /* + * Total number of properties is equal to static properties + * plus the dynamic properties.Some properties may not be set + * as come channels may be not be supported by the device.So + * we need to take care of that. + */ + psy_desc->properties = properties; + psy_desc->num_properties = index; + + adc_bat->psy = power_supply_register(&pdev->dev, psy_desc, &psy_cfg); + if (IS_ERR(adc_bat->psy)) { + ret = PTR_ERR(adc_bat->psy); + goto err_reg_fail; + } + + INIT_DELAYED_WORK(&adc_bat->bat_work, gab_work); + + if (gpio_is_valid(pdata->gpio_charge_finished)) { + int irq; + ret = gpio_request(pdata->gpio_charge_finished, "charged"); + if (ret) + goto gpio_req_fail; + + irq = gpio_to_irq(pdata->gpio_charge_finished); + ret = request_any_context_irq(irq, gab_charged, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "battery charged", adc_bat); + if (ret < 0) + goto err_gpio; + } + + platform_set_drvdata(pdev, adc_bat); + + /* Schedule timer to check current status */ + schedule_delayed_work(&adc_bat->bat_work, + msecs_to_jiffies(0)); + return 0; + +err_gpio: + gpio_free(pdata->gpio_charge_finished); +gpio_req_fail: + power_supply_unregister(adc_bat->psy); +err_reg_fail: + for (chan = 0; chan < ARRAY_SIZE(gab_chan_name); chan++) { + if (adc_bat->channel[chan]) + iio_channel_release(adc_bat->channel[chan]); + } +second_mem_fail: + kfree(properties); +first_mem_fail: + return ret; +} + +static int gab_remove(struct platform_device *pdev) +{ + int chan; + struct gab *adc_bat = platform_get_drvdata(pdev); + struct gab_platform_data *pdata = adc_bat->pdata; + + power_supply_unregister(adc_bat->psy); + + if (gpio_is_valid(pdata->gpio_charge_finished)) { + free_irq(gpio_to_irq(pdata->gpio_charge_finished), adc_bat); + gpio_free(pdata->gpio_charge_finished); + } + + for (chan = 0; chan < ARRAY_SIZE(gab_chan_name); chan++) { + if (adc_bat->channel[chan]) + iio_channel_release(adc_bat->channel[chan]); + } + + kfree(adc_bat->psy_desc.properties); + cancel_delayed_work_sync(&adc_bat->bat_work); + return 0; +} + +static int __maybe_unused gab_suspend(struct device *dev) +{ + struct gab *adc_bat = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&adc_bat->bat_work); + adc_bat->status = POWER_SUPPLY_STATUS_UNKNOWN; + return 0; +} + +static int __maybe_unused gab_resume(struct device *dev) +{ + struct gab *adc_bat = dev_get_drvdata(dev); + struct gab_platform_data *pdata = adc_bat->pdata; + int delay; + + delay = pdata->jitter_delay ? pdata->jitter_delay : JITTER_DEFAULT; + + /* Schedule timer to check current status */ + schedule_delayed_work(&adc_bat->bat_work, + msecs_to_jiffies(delay)); + return 0; +} + +static SIMPLE_DEV_PM_OPS(gab_pm_ops, gab_suspend, gab_resume); + +static struct platform_driver gab_driver = { + .driver = { + .name = "generic-adc-battery", + .pm = &gab_pm_ops, + }, + .probe = gab_probe, + .remove = gab_remove, +}; +module_platform_driver(gab_driver); + +MODULE_AUTHOR("anish kumar <anish198519851985@gmail.com>"); +MODULE_DESCRIPTION("generic battery driver using IIO"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/goldfish_battery.c b/drivers/power/supply/goldfish_battery.c new file mode 100644 index 000000000..bf1754355 --- /dev/null +++ b/drivers/power/supply/goldfish_battery.c @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Power supply driver for the goldfish emulator + * + * Copyright (C) 2008 Google, Inc. + * Copyright (C) 2012 Intel, Inc. + * Copyright (C) 2013 Intel, Inc. + * Author: Mike Lockwood <lockwood@android.com> + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/types.h> +#include <linux/pci.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/acpi.h> + +struct goldfish_battery_data { + void __iomem *reg_base; + int irq; + spinlock_t lock; + + struct power_supply *battery; + struct power_supply *ac; +}; + +#define GOLDFISH_BATTERY_READ(data, addr) \ + (readl(data->reg_base + addr)) +#define GOLDFISH_BATTERY_WRITE(data, addr, x) \ + (writel(x, data->reg_base + addr)) + +enum { + /* status register */ + BATTERY_INT_STATUS = 0x00, + /* set this to enable IRQ */ + BATTERY_INT_ENABLE = 0x04, + + BATTERY_AC_ONLINE = 0x08, + BATTERY_STATUS = 0x0C, + BATTERY_HEALTH = 0x10, + BATTERY_PRESENT = 0x14, + BATTERY_CAPACITY = 0x18, + BATTERY_VOLTAGE = 0x1C, + BATTERY_TEMP = 0x20, + BATTERY_CHARGE_COUNTER = 0x24, + BATTERY_VOLTAGE_MAX = 0x28, + BATTERY_CURRENT_MAX = 0x2C, + BATTERY_CURRENT_NOW = 0x30, + BATTERY_CURRENT_AVG = 0x34, + BATTERY_CHARGE_FULL_UAH = 0x38, + BATTERY_CYCLE_COUNT = 0x40, + + BATTERY_STATUS_CHANGED = 1U << 0, + AC_STATUS_CHANGED = 1U << 1, + BATTERY_INT_MASK = BATTERY_STATUS_CHANGED | AC_STATUS_CHANGED, +}; + + +static int goldfish_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct goldfish_battery_data *data = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_AC_ONLINE); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_VOLTAGE_MAX); + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_CURRENT_MAX); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static int goldfish_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct goldfish_battery_data *data = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_STATUS); + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_HEALTH); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_PRESENT); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_CAPACITY); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_VOLTAGE); + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_TEMP); + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + val->intval = GOLDFISH_BATTERY_READ(data, + BATTERY_CHARGE_COUNTER); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_CURRENT_NOW); + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_CURRENT_AVG); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = GOLDFISH_BATTERY_READ(data, + BATTERY_CHARGE_FULL_UAH); + break; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + val->intval = GOLDFISH_BATTERY_READ(data, BATTERY_CYCLE_COUNT); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property goldfish_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CYCLE_COUNT, +}; + +static enum power_supply_property goldfish_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static irqreturn_t goldfish_battery_interrupt(int irq, void *dev_id) +{ + unsigned long irq_flags; + struct goldfish_battery_data *data = dev_id; + uint32_t status; + + spin_lock_irqsave(&data->lock, irq_flags); + + /* read status flags, which will clear the interrupt */ + status = GOLDFISH_BATTERY_READ(data, BATTERY_INT_STATUS); + status &= BATTERY_INT_MASK; + + if (status & BATTERY_STATUS_CHANGED) + power_supply_changed(data->battery); + if (status & AC_STATUS_CHANGED) + power_supply_changed(data->ac); + + spin_unlock_irqrestore(&data->lock, irq_flags); + return status ? IRQ_HANDLED : IRQ_NONE; +} + +static const struct power_supply_desc battery_desc = { + .properties = goldfish_battery_props, + .num_properties = ARRAY_SIZE(goldfish_battery_props), + .get_property = goldfish_battery_get_property, + .name = "battery", + .type = POWER_SUPPLY_TYPE_BATTERY, +}; + +static const struct power_supply_desc ac_desc = { + .properties = goldfish_ac_props, + .num_properties = ARRAY_SIZE(goldfish_ac_props), + .get_property = goldfish_ac_get_property, + .name = "ac", + .type = POWER_SUPPLY_TYPE_MAINS, +}; + +static int goldfish_battery_probe(struct platform_device *pdev) +{ + int ret; + struct resource *r; + struct goldfish_battery_data *data; + struct power_supply_config psy_cfg = {}; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (data == NULL) + return -ENOMEM; + + spin_lock_init(&data->lock); + + r = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (r == NULL) { + dev_err(&pdev->dev, "platform_get_resource failed\n"); + return -ENODEV; + } + + data->reg_base = devm_ioremap(&pdev->dev, r->start, resource_size(r)); + if (data->reg_base == NULL) { + dev_err(&pdev->dev, "unable to remap MMIO\n"); + return -ENOMEM; + } + + data->irq = platform_get_irq(pdev, 0); + if (data->irq < 0) { + dev_err(&pdev->dev, "platform_get_irq failed\n"); + return -ENODEV; + } + + ret = devm_request_irq(&pdev->dev, data->irq, + goldfish_battery_interrupt, + IRQF_SHARED, pdev->name, data); + if (ret) + return ret; + + psy_cfg.drv_data = data; + + data->ac = power_supply_register(&pdev->dev, &ac_desc, &psy_cfg); + if (IS_ERR(data->ac)) + return PTR_ERR(data->ac); + + data->battery = power_supply_register(&pdev->dev, &battery_desc, + &psy_cfg); + if (IS_ERR(data->battery)) { + power_supply_unregister(data->ac); + return PTR_ERR(data->battery); + } + + platform_set_drvdata(pdev, data); + + GOLDFISH_BATTERY_WRITE(data, BATTERY_INT_ENABLE, BATTERY_INT_MASK); + return 0; +} + +static int goldfish_battery_remove(struct platform_device *pdev) +{ + struct goldfish_battery_data *data = platform_get_drvdata(pdev); + + power_supply_unregister(data->battery); + power_supply_unregister(data->ac); + return 0; +} + +static const struct of_device_id goldfish_battery_of_match[] = { + { .compatible = "google,goldfish-battery", }, + {}, +}; +MODULE_DEVICE_TABLE(of, goldfish_battery_of_match); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id goldfish_battery_acpi_match[] = { + { "GFSH0001", 0 }, + { }, +}; +MODULE_DEVICE_TABLE(acpi, goldfish_battery_acpi_match); +#endif + +static struct platform_driver goldfish_battery_device = { + .probe = goldfish_battery_probe, + .remove = goldfish_battery_remove, + .driver = { + .name = "goldfish-battery", + .of_match_table = goldfish_battery_of_match, + .acpi_match_table = ACPI_PTR(goldfish_battery_acpi_match), + } +}; +module_platform_driver(goldfish_battery_device); + +MODULE_AUTHOR("Mike Lockwood lockwood@android.com"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Battery driver for the Goldfish emulator"); diff --git a/drivers/power/supply/gpio-charger.c b/drivers/power/supply/gpio-charger.c new file mode 100644 index 000000000..68212b397 --- /dev/null +++ b/drivers/power/supply/gpio-charger.c @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2010, Lars-Peter Clausen <lars@metafoo.de> + * Driver for chargers which report their online status through a GPIO pin + */ + +#include <linux/device.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/of.h> +#include <linux/gpio/consumer.h> + +#include <linux/power/gpio-charger.h> + +struct gpio_mapping { + u32 limit_ua; + u32 gpiodata; +} __packed; + +struct gpio_charger { + struct device *dev; + unsigned int irq; + unsigned int charge_status_irq; + bool wakeup_enabled; + + struct power_supply *charger; + struct power_supply_desc charger_desc; + struct gpio_desc *gpiod; + struct gpio_desc *charge_status; + + struct gpio_descs *current_limit_gpios; + struct gpio_mapping *current_limit_map; + u32 current_limit_map_size; + u32 charge_current_limit; +}; + +static irqreturn_t gpio_charger_irq(int irq, void *devid) +{ + struct power_supply *charger = devid; + + power_supply_changed(charger); + + return IRQ_HANDLED; +} + +static inline struct gpio_charger *psy_to_gpio_charger(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static int set_charge_current_limit(struct gpio_charger *gpio_charger, int val) +{ + struct gpio_mapping mapping; + int ndescs = gpio_charger->current_limit_gpios->ndescs; + struct gpio_desc **gpios = gpio_charger->current_limit_gpios->desc; + int i; + + if (!gpio_charger->current_limit_map_size) + return -EINVAL; + + for (i = 0; i < gpio_charger->current_limit_map_size; i++) { + if (gpio_charger->current_limit_map[i].limit_ua <= val) + break; + } + mapping = gpio_charger->current_limit_map[i]; + + for (i = 0; i < ndescs; i++) { + bool val = (mapping.gpiodata >> i) & 1; + gpiod_set_value_cansleep(gpios[ndescs-i-1], val); + } + + gpio_charger->charge_current_limit = mapping.limit_ua; + + dev_dbg(gpio_charger->dev, "set charge current limit to %d (requested: %d)\n", + gpio_charger->charge_current_limit, val); + + return 0; +} + +static int gpio_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct gpio_charger *gpio_charger = psy_to_gpio_charger(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = gpiod_get_value_cansleep(gpio_charger->gpiod); + break; + case POWER_SUPPLY_PROP_STATUS: + if (gpiod_get_value_cansleep(gpio_charger->charge_status)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = gpio_charger->charge_current_limit; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int gpio_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, const union power_supply_propval *val) +{ + struct gpio_charger *gpio_charger = psy_to_gpio_charger(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + return set_charge_current_limit(gpio_charger, val->intval); + default: + return -EINVAL; + } + + return 0; +} + +static int gpio_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + return 1; + default: + break; + } + + return 0; +} + +static enum power_supply_type gpio_charger_get_type(struct device *dev) +{ + const char *chargetype; + + if (!device_property_read_string(dev, "charger-type", &chargetype)) { + if (!strcmp("unknown", chargetype)) + return POWER_SUPPLY_TYPE_UNKNOWN; + if (!strcmp("battery", chargetype)) + return POWER_SUPPLY_TYPE_BATTERY; + if (!strcmp("ups", chargetype)) + return POWER_SUPPLY_TYPE_UPS; + if (!strcmp("mains", chargetype)) + return POWER_SUPPLY_TYPE_MAINS; + if (!strcmp("usb-sdp", chargetype)) + return POWER_SUPPLY_TYPE_USB; + if (!strcmp("usb-dcp", chargetype)) + return POWER_SUPPLY_TYPE_USB; + if (!strcmp("usb-cdp", chargetype)) + return POWER_SUPPLY_TYPE_USB; + if (!strcmp("usb-aca", chargetype)) + return POWER_SUPPLY_TYPE_USB; + } + dev_warn(dev, "unknown charger type %s\n", chargetype); + + return POWER_SUPPLY_TYPE_UNKNOWN; +} + +static int gpio_charger_get_irq(struct device *dev, void *dev_id, + struct gpio_desc *gpio) +{ + int ret, irq = gpiod_to_irq(gpio); + + if (irq > 0) { + ret = devm_request_any_context_irq(dev, irq, gpio_charger_irq, + IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING, + dev_name(dev), + dev_id); + if (ret < 0) { + dev_warn(dev, "Failed to request irq: %d\n", ret); + irq = 0; + } + } + + return irq; +} + +static int init_charge_current_limit(struct device *dev, + struct gpio_charger *gpio_charger) +{ + int i, len; + u32 cur_limit = U32_MAX; + + gpio_charger->current_limit_gpios = devm_gpiod_get_array_optional(dev, + "charge-current-limit", GPIOD_OUT_LOW); + if (IS_ERR(gpio_charger->current_limit_gpios)) { + dev_err(dev, "error getting current-limit GPIOs\n"); + return PTR_ERR(gpio_charger->current_limit_gpios); + } + + if (!gpio_charger->current_limit_gpios) + return 0; + + len = device_property_read_u32_array(dev, "charge-current-limit-mapping", + NULL, 0); + if (len < 0) + return len; + + if (len == 0 || len % 2) { + dev_err(dev, "invalid charge-current-limit-mapping length\n"); + return -EINVAL; + } + + gpio_charger->current_limit_map = devm_kmalloc_array(dev, + len / 2, sizeof(*gpio_charger->current_limit_map), GFP_KERNEL); + if (!gpio_charger->current_limit_map) + return -ENOMEM; + + gpio_charger->current_limit_map_size = len / 2; + + len = device_property_read_u32_array(dev, "charge-current-limit-mapping", + (u32*) gpio_charger->current_limit_map, len); + if (len < 0) + return len; + + for (i=0; i < gpio_charger->current_limit_map_size; i++) { + if (gpio_charger->current_limit_map[i].limit_ua > cur_limit) { + dev_err(dev, "charge-current-limit-mapping not sorted by current in descending order\n"); + return -EINVAL; + } + + cur_limit = gpio_charger->current_limit_map[i].limit_ua; + } + + /* default to smallest current limitation for safety reasons */ + len = gpio_charger->current_limit_map_size - 1; + set_charge_current_limit(gpio_charger, + gpio_charger->current_limit_map[len].limit_ua); + + return 0; +} + +/* + * The entries will be overwritten by driver's probe routine depending + * on the available features. This list ensures, that the array is big + * enough for all optional features. + */ +static enum power_supply_property gpio_charger_properties[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, +}; + +static int gpio_charger_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + const struct gpio_charger_platform_data *pdata = dev->platform_data; + struct power_supply_config psy_cfg = {}; + struct gpio_charger *gpio_charger; + struct power_supply_desc *charger_desc; + struct gpio_desc *charge_status; + int charge_status_irq; + int ret; + int num_props = 0; + + if (!pdata && !dev->of_node) { + dev_err(dev, "No platform data\n"); + return -ENOENT; + } + + gpio_charger = devm_kzalloc(dev, sizeof(*gpio_charger), GFP_KERNEL); + if (!gpio_charger) + return -ENOMEM; + gpio_charger->dev = dev; + + /* + * This will fetch a GPIO descriptor from device tree, ACPI or + * boardfile descriptor tables. It's good to try this first. + */ + gpio_charger->gpiod = devm_gpiod_get_optional(dev, NULL, GPIOD_IN); + if (IS_ERR(gpio_charger->gpiod)) { + /* Just try again if this happens */ + return dev_err_probe(dev, PTR_ERR(gpio_charger->gpiod), + "error getting GPIO descriptor\n"); + } + + if (gpio_charger->gpiod) { + gpio_charger_properties[num_props] = POWER_SUPPLY_PROP_ONLINE; + num_props++; + } + + charge_status = devm_gpiod_get_optional(dev, "charge-status", GPIOD_IN); + if (IS_ERR(charge_status)) + return PTR_ERR(charge_status); + if (charge_status) { + gpio_charger->charge_status = charge_status; + gpio_charger_properties[num_props] = POWER_SUPPLY_PROP_STATUS; + num_props++; + } + + ret = init_charge_current_limit(dev, gpio_charger); + if (ret < 0) + return ret; + if (gpio_charger->current_limit_map) { + gpio_charger_properties[num_props] = + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX; + num_props++; + } + + charger_desc = &gpio_charger->charger_desc; + charger_desc->properties = gpio_charger_properties; + charger_desc->num_properties = num_props; + charger_desc->get_property = gpio_charger_get_property; + charger_desc->set_property = gpio_charger_set_property; + charger_desc->property_is_writeable = + gpio_charger_property_is_writeable; + + psy_cfg.of_node = dev->of_node; + psy_cfg.drv_data = gpio_charger; + + if (pdata) { + charger_desc->name = pdata->name; + charger_desc->type = pdata->type; + psy_cfg.supplied_to = pdata->supplied_to; + psy_cfg.num_supplicants = pdata->num_supplicants; + } else { + charger_desc->name = dev->of_node->name; + charger_desc->type = gpio_charger_get_type(dev); + } + + if (!charger_desc->name) + charger_desc->name = pdev->name; + + gpio_charger->charger = devm_power_supply_register(dev, charger_desc, + &psy_cfg); + if (IS_ERR(gpio_charger->charger)) { + ret = PTR_ERR(gpio_charger->charger); + dev_err(dev, "Failed to register power supply: %d\n", ret); + return ret; + } + + gpio_charger->irq = gpio_charger_get_irq(dev, gpio_charger->charger, + gpio_charger->gpiod); + + charge_status_irq = gpio_charger_get_irq(dev, gpio_charger->charger, + gpio_charger->charge_status); + gpio_charger->charge_status_irq = charge_status_irq; + + platform_set_drvdata(pdev, gpio_charger); + + device_init_wakeup(dev, 1); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int gpio_charger_suspend(struct device *dev) +{ + struct gpio_charger *gpio_charger = dev_get_drvdata(dev); + + if (device_may_wakeup(dev)) + gpio_charger->wakeup_enabled = + !enable_irq_wake(gpio_charger->irq); + + return 0; +} + +static int gpio_charger_resume(struct device *dev) +{ + struct gpio_charger *gpio_charger = dev_get_drvdata(dev); + + if (device_may_wakeup(dev) && gpio_charger->wakeup_enabled) + disable_irq_wake(gpio_charger->irq); + power_supply_changed(gpio_charger->charger); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(gpio_charger_pm_ops, + gpio_charger_suspend, gpio_charger_resume); + +static const struct of_device_id gpio_charger_match[] = { + { .compatible = "gpio-charger" }, + { } +}; +MODULE_DEVICE_TABLE(of, gpio_charger_match); + +static struct platform_driver gpio_charger_driver = { + .probe = gpio_charger_probe, + .driver = { + .name = "gpio-charger", + .pm = &gpio_charger_pm_ops, + .of_match_table = gpio_charger_match, + }, +}; + +module_platform_driver(gpio_charger_driver); + +MODULE_AUTHOR("Lars-Peter Clausen <lars@metafoo.de>"); +MODULE_DESCRIPTION("Driver for chargers only communicating via GPIO(s)"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:gpio-charger"); diff --git a/drivers/power/supply/ingenic-battery.c b/drivers/power/supply/ingenic-battery.c new file mode 100644 index 000000000..32dc77fd9 --- /dev/null +++ b/drivers/power/supply/ingenic-battery.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Battery driver for the Ingenic JZ47xx SoCs + * Copyright (c) 2019 Artur Rojek <contact@artur-rojek.eu> + * + * based on drivers/power/supply/jz4740-battery.c + */ + +#include <linux/iio/consumer.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/property.h> + +struct ingenic_battery { + struct device *dev; + struct iio_channel *channel; + struct power_supply_desc desc; + struct power_supply *battery; + struct power_supply_battery_info info; +}; + +static int ingenic_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ingenic_battery *bat = power_supply_get_drvdata(psy); + struct power_supply_battery_info *info = &bat->info; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + ret = iio_read_channel_processed(bat->channel, &val->intval); + val->intval *= 1000; + if (val->intval < info->voltage_min_design_uv) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (val->intval > info->voltage_max_design_uv) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return ret; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = iio_read_channel_processed(bat->channel, &val->intval); + val->intval *= 1000; + return ret; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = info->voltage_min_design_uv; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = info->voltage_max_design_uv; + return 0; + default: + return -EINVAL; + }; +} + +/* Set the most appropriate IIO channel voltage reference scale + * based on the battery's max voltage. + */ +static int ingenic_battery_set_scale(struct ingenic_battery *bat) +{ + const int *scale_raw; + int scale_len, scale_type, best_idx = -1, best_mV, max_raw, i, ret; + u64 max_mV; + + ret = iio_read_max_channel_raw(bat->channel, &max_raw); + if (ret) { + dev_err(bat->dev, "Unable to read max raw channel value\n"); + return ret; + } + + ret = iio_read_avail_channel_attribute(bat->channel, &scale_raw, + &scale_type, &scale_len, + IIO_CHAN_INFO_SCALE); + if (ret < 0) { + dev_err(bat->dev, "Unable to read channel avail scale\n"); + return ret; + } + if (ret != IIO_AVAIL_LIST || scale_type != IIO_VAL_FRACTIONAL_LOG2) + return -EINVAL; + + max_mV = bat->info.voltage_max_design_uv / 1000; + + for (i = 0; i < scale_len; i += 2) { + u64 scale_mV = (max_raw * scale_raw[i]) >> scale_raw[i + 1]; + + if (scale_mV < max_mV) + continue; + + if (best_idx >= 0 && scale_mV > best_mV) + continue; + + best_mV = scale_mV; + best_idx = i; + } + + if (best_idx < 0) { + dev_err(bat->dev, "Unable to find matching voltage scale\n"); + return -EINVAL; + } + + /* Only set scale if there is more than one (fractional) entry */ + if (scale_len > 2) { + ret = iio_write_channel_attribute(bat->channel, + scale_raw[best_idx], + scale_raw[best_idx + 1], + IIO_CHAN_INFO_SCALE); + if (ret) + return ret; + } + + return 0; +} + +static enum power_supply_property ingenic_battery_properties[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, +}; + +static int ingenic_battery_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ingenic_battery *bat; + struct power_supply_config psy_cfg = {}; + struct power_supply_desc *desc; + int ret; + + bat = devm_kzalloc(dev, sizeof(*bat), GFP_KERNEL); + if (!bat) + return -ENOMEM; + + bat->dev = dev; + bat->channel = devm_iio_channel_get(dev, "battery"); + if (IS_ERR(bat->channel)) + return PTR_ERR(bat->channel); + + desc = &bat->desc; + desc->name = "jz-battery"; + desc->type = POWER_SUPPLY_TYPE_BATTERY; + desc->properties = ingenic_battery_properties; + desc->num_properties = ARRAY_SIZE(ingenic_battery_properties); + desc->get_property = ingenic_battery_get_property; + psy_cfg.drv_data = bat; + psy_cfg.of_node = dev->of_node; + + bat->battery = devm_power_supply_register(dev, desc, &psy_cfg); + if (IS_ERR(bat->battery)) + return dev_err_probe(dev, PTR_ERR(bat->battery), + "Unable to register battery\n"); + + ret = power_supply_get_battery_info(bat->battery, &bat->info); + if (ret) { + dev_err(dev, "Unable to get battery info: %d\n", ret); + return ret; + } + if (bat->info.voltage_min_design_uv < 0) { + dev_err(dev, "Unable to get voltage min design\n"); + return bat->info.voltage_min_design_uv; + } + if (bat->info.voltage_max_design_uv < 0) { + dev_err(dev, "Unable to get voltage max design\n"); + return bat->info.voltage_max_design_uv; + } + + return ingenic_battery_set_scale(bat); +} + +#ifdef CONFIG_OF +static const struct of_device_id ingenic_battery_of_match[] = { + { .compatible = "ingenic,jz4740-battery", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ingenic_battery_of_match); +#endif + +static struct platform_driver ingenic_battery_driver = { + .driver = { + .name = "ingenic-battery", + .of_match_table = of_match_ptr(ingenic_battery_of_match), + }, + .probe = ingenic_battery_probe, +}; +module_platform_driver(ingenic_battery_driver); + +MODULE_DESCRIPTION("Battery driver for Ingenic JZ47xx SoCs"); +MODULE_AUTHOR("Artur Rojek <contact@artur-rojek.eu>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/ipaq_micro_battery.c b/drivers/power/supply/ipaq_micro_battery.c new file mode 100644 index 000000000..192d9db0f --- /dev/null +++ b/drivers/power/supply/ipaq_micro_battery.c @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * + * h3xxx atmel micro companion support, battery subdevice + * based on previous kernel 2.4 version + * Author : Alessandro Gardich <gremlin@gremlin.it> + * Author : Linus Walleij <linus.walleij@linaro.org> + */ + +#include <linux/module.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/mfd/ipaq-micro.h> +#include <linux/power_supply.h> +#include <linux/workqueue.h> + +#define BATT_PERIOD 100000 /* 100 seconds in milliseconds */ + +#define MICRO_BATT_CHEM_ALKALINE 0x01 +#define MICRO_BATT_CHEM_NICD 0x02 +#define MICRO_BATT_CHEM_NIMH 0x03 +#define MICRO_BATT_CHEM_LION 0x04 +#define MICRO_BATT_CHEM_LIPOLY 0x05 +#define MICRO_BATT_CHEM_NOT_INSTALLED 0x06 +#define MICRO_BATT_CHEM_UNKNOWN 0xff + +#define MICRO_BATT_STATUS_HIGH 0x01 +#define MICRO_BATT_STATUS_LOW 0x02 +#define MICRO_BATT_STATUS_CRITICAL 0x04 +#define MICRO_BATT_STATUS_CHARGING 0x08 +#define MICRO_BATT_STATUS_CHARGEMAIN 0x10 +#define MICRO_BATT_STATUS_DEAD 0x20 /* Battery will not charge */ +#define MICRO_BATT_STATUS_NOTINSTALLED 0x20 /* For expansion pack batteries */ +#define MICRO_BATT_STATUS_FULL 0x40 /* Battery fully charged */ +#define MICRO_BATT_STATUS_NOBATTERY 0x80 +#define MICRO_BATT_STATUS_UNKNOWN 0xff + +struct micro_battery { + struct ipaq_micro *micro; + struct workqueue_struct *wq; + struct delayed_work update; + u8 ac; + u8 chemistry; + unsigned int voltage; + u16 temperature; + u8 flag; +}; + +static void micro_battery_work(struct work_struct *work) +{ + struct micro_battery *mb = container_of(work, + struct micro_battery, update.work); + struct ipaq_micro_msg msg_battery = { + .id = MSG_BATTERY, + }; + struct ipaq_micro_msg msg_sensor = { + .id = MSG_THERMAL_SENSOR, + }; + + /* First send battery message */ + ipaq_micro_tx_msg_sync(mb->micro, &msg_battery); + if (msg_battery.rx_len < 4) + pr_info("ERROR"); + + /* + * Returned message format: + * byte 0: 0x00 = Not plugged in + * 0x01 = AC adapter plugged in + * byte 1: chemistry + * byte 2: voltage LSB + * byte 3: voltage MSB + * byte 4: flags + * byte 5-9: same for battery 2 + */ + mb->ac = msg_battery.rx_data[0]; + mb->chemistry = msg_battery.rx_data[1]; + mb->voltage = ((((unsigned short)msg_battery.rx_data[3] << 8) + + msg_battery.rx_data[2]) * 5000L) * 1000 / 1024; + mb->flag = msg_battery.rx_data[4]; + + if (msg_battery.rx_len == 9) + pr_debug("second battery ignored\n"); + + /* Then read the sensor */ + ipaq_micro_tx_msg_sync(mb->micro, &msg_sensor); + mb->temperature = msg_sensor.rx_data[1] << 8 | msg_sensor.rx_data[0]; + + queue_delayed_work(mb->wq, &mb->update, msecs_to_jiffies(BATT_PERIOD)); +} + +static int get_capacity(struct power_supply *b) +{ + struct micro_battery *mb = dev_get_drvdata(b->dev.parent); + + switch (mb->flag & 0x07) { + case MICRO_BATT_STATUS_HIGH: + return 100; + break; + case MICRO_BATT_STATUS_LOW: + return 50; + break; + case MICRO_BATT_STATUS_CRITICAL: + return 5; + break; + default: + break; + } + return 0; +} + +static int get_status(struct power_supply *b) +{ + struct micro_battery *mb = dev_get_drvdata(b->dev.parent); + + if (mb->flag == MICRO_BATT_STATUS_UNKNOWN) + return POWER_SUPPLY_STATUS_UNKNOWN; + + if (mb->flag & MICRO_BATT_STATUS_FULL) + return POWER_SUPPLY_STATUS_FULL; + + if ((mb->flag & MICRO_BATT_STATUS_CHARGING) || + (mb->flag & MICRO_BATT_STATUS_CHARGEMAIN)) + return POWER_SUPPLY_STATUS_CHARGING; + + return POWER_SUPPLY_STATUS_DISCHARGING; +} + +static int micro_batt_get_property(struct power_supply *b, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct micro_battery *mb = dev_get_drvdata(b->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_TECHNOLOGY: + switch (mb->chemistry) { + case MICRO_BATT_CHEM_NICD: + val->intval = POWER_SUPPLY_TECHNOLOGY_NiCd; + break; + case MICRO_BATT_CHEM_NIMH: + val->intval = POWER_SUPPLY_TECHNOLOGY_NiMH; + break; + case MICRO_BATT_CHEM_LION: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case MICRO_BATT_CHEM_LIPOLY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LIPO; + break; + default: + val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + break; + } + break; + case POWER_SUPPLY_PROP_STATUS: + val->intval = get_status(b); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = 4700000; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = get_capacity(b); + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = mb->temperature; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = mb->voltage; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int micro_ac_get_property(struct power_supply *b, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct micro_battery *mb = dev_get_drvdata(b->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = mb->ac; + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property micro_batt_power_props[] = { + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static const struct power_supply_desc micro_batt_power_desc = { + .name = "main-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = micro_batt_power_props, + .num_properties = ARRAY_SIZE(micro_batt_power_props), + .get_property = micro_batt_get_property, + .use_for_apm = 1, +}; + +static enum power_supply_property micro_ac_power_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc micro_ac_power_desc = { + .name = "ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = micro_ac_power_props, + .num_properties = ARRAY_SIZE(micro_ac_power_props), + .get_property = micro_ac_get_property, +}; + +static struct power_supply *micro_batt_power, *micro_ac_power; + +static int micro_batt_probe(struct platform_device *pdev) +{ + struct micro_battery *mb; + int ret; + + mb = devm_kzalloc(&pdev->dev, sizeof(*mb), GFP_KERNEL); + if (!mb) + return -ENOMEM; + + mb->micro = dev_get_drvdata(pdev->dev.parent); + mb->wq = alloc_workqueue("ipaq-battery-wq", WQ_MEM_RECLAIM, 0); + if (!mb->wq) + return -ENOMEM; + + INIT_DELAYED_WORK(&mb->update, micro_battery_work); + platform_set_drvdata(pdev, mb); + queue_delayed_work(mb->wq, &mb->update, 1); + + micro_batt_power = power_supply_register(&pdev->dev, + µ_batt_power_desc, NULL); + if (IS_ERR(micro_batt_power)) { + ret = PTR_ERR(micro_batt_power); + goto batt_err; + } + + micro_ac_power = power_supply_register(&pdev->dev, + µ_ac_power_desc, NULL); + if (IS_ERR(micro_ac_power)) { + ret = PTR_ERR(micro_ac_power); + goto ac_err; + } + + dev_info(&pdev->dev, "iPAQ micro battery driver\n"); + return 0; + +ac_err: + power_supply_unregister(micro_batt_power); +batt_err: + cancel_delayed_work_sync(&mb->update); + destroy_workqueue(mb->wq); + return ret; +} + +static int micro_batt_remove(struct platform_device *pdev) + +{ + struct micro_battery *mb = platform_get_drvdata(pdev); + + power_supply_unregister(micro_ac_power); + power_supply_unregister(micro_batt_power); + cancel_delayed_work_sync(&mb->update); + destroy_workqueue(mb->wq); + + return 0; +} + +static int __maybe_unused micro_batt_suspend(struct device *dev) +{ + struct micro_battery *mb = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&mb->update); + return 0; +} + +static int __maybe_unused micro_batt_resume(struct device *dev) +{ + struct micro_battery *mb = dev_get_drvdata(dev); + + queue_delayed_work(mb->wq, &mb->update, msecs_to_jiffies(BATT_PERIOD)); + return 0; +} + +static const struct dev_pm_ops micro_batt_dev_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(micro_batt_suspend, micro_batt_resume) +}; + +static struct platform_driver micro_batt_device_driver = { + .driver = { + .name = "ipaq-micro-battery", + .pm = µ_batt_dev_pm_ops, + }, + .probe = micro_batt_probe, + .remove = micro_batt_remove, +}; +module_platform_driver(micro_batt_device_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("driver for iPAQ Atmel micro battery"); +MODULE_ALIAS("platform:ipaq-micro-battery"); diff --git a/drivers/power/supply/isp1704_charger.c b/drivers/power/supply/isp1704_charger.c new file mode 100644 index 000000000..b6efc454e --- /dev/null +++ b/drivers/power/supply/isp1704_charger.c @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ISP1704 USB Charger Detection driver + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2012 - 2013 Pali Rohár <pali@kernel.org> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/device.h> +#include <linux/sysfs.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/delay.h> +#include <linux/of.h> + +#include <linux/gpio/consumer.h> +#include <linux/usb/otg.h> +#include <linux/usb/ulpi.h> +#include <linux/usb/ch9.h> +#include <linux/usb/gadget.h> + +/* Vendor specific Power Control register */ +#define ISP1704_PWR_CTRL 0x3d +#define ISP1704_PWR_CTRL_SWCTRL (1 << 0) +#define ISP1704_PWR_CTRL_DET_COMP (1 << 1) +#define ISP1704_PWR_CTRL_BVALID_RISE (1 << 2) +#define ISP1704_PWR_CTRL_BVALID_FALL (1 << 3) +#define ISP1704_PWR_CTRL_DP_WKPU_EN (1 << 4) +#define ISP1704_PWR_CTRL_VDAT_DET (1 << 5) +#define ISP1704_PWR_CTRL_DPVSRC_EN (1 << 6) +#define ISP1704_PWR_CTRL_HWDETECT (1 << 7) + +#define NXP_VENDOR_ID 0x04cc + +static u16 isp170x_id[] = { + 0x1704, + 0x1707, +}; + +struct isp1704_charger { + struct device *dev; + struct power_supply *psy; + struct power_supply_desc psy_desc; + struct gpio_desc *enable_gpio; + struct usb_phy *phy; + struct notifier_block nb; + struct work_struct work; + + /* properties */ + char model[8]; + unsigned present:1; + unsigned online:1; + unsigned current_max; +}; + +static inline int isp1704_read(struct isp1704_charger *isp, u32 reg) +{ + return usb_phy_io_read(isp->phy, reg); +} + +static inline int isp1704_write(struct isp1704_charger *isp, u32 reg, u32 val) +{ + return usb_phy_io_write(isp->phy, val, reg); +} + +static void isp1704_charger_set_power(struct isp1704_charger *isp, bool on) +{ + gpiod_set_value(isp->enable_gpio, on); +} + +/* + * Determine is the charging port DCP (dedicated charger) or CDP (Host/HUB + * chargers). + * + * REVISIT: The method is defined in Battery Charging Specification and is + * applicable to any ULPI transceiver. Nothing isp170x specific here. + */ +static inline int isp1704_charger_type(struct isp1704_charger *isp) +{ + u8 reg; + u8 func_ctrl; + u8 otg_ctrl; + int type = POWER_SUPPLY_TYPE_USB_DCP; + + func_ctrl = isp1704_read(isp, ULPI_FUNC_CTRL); + otg_ctrl = isp1704_read(isp, ULPI_OTG_CTRL); + + /* disable pulldowns */ + reg = ULPI_OTG_CTRL_DM_PULLDOWN | ULPI_OTG_CTRL_DP_PULLDOWN; + isp1704_write(isp, ULPI_CLR(ULPI_OTG_CTRL), reg); + + /* full speed */ + isp1704_write(isp, ULPI_CLR(ULPI_FUNC_CTRL), + ULPI_FUNC_CTRL_XCVRSEL_MASK); + isp1704_write(isp, ULPI_SET(ULPI_FUNC_CTRL), + ULPI_FUNC_CTRL_FULL_SPEED); + + /* Enable strong pull-up on DP (1.5K) and reset */ + reg = ULPI_FUNC_CTRL_TERMSELECT | ULPI_FUNC_CTRL_RESET; + isp1704_write(isp, ULPI_SET(ULPI_FUNC_CTRL), reg); + usleep_range(1000, 2000); + + reg = isp1704_read(isp, ULPI_DEBUG); + if ((reg & 3) != 3) + type = POWER_SUPPLY_TYPE_USB_CDP; + + /* recover original state */ + isp1704_write(isp, ULPI_FUNC_CTRL, func_ctrl); + isp1704_write(isp, ULPI_OTG_CTRL, otg_ctrl); + + return type; +} + +/* + * ISP1704 detects PS/2 adapters as charger. To make sure the detected charger + * is actually a dedicated charger, the following steps need to be taken. + */ +static inline int isp1704_charger_verify(struct isp1704_charger *isp) +{ + int ret = 0; + u8 r; + + /* Reset the transceiver */ + r = isp1704_read(isp, ULPI_FUNC_CTRL); + r |= ULPI_FUNC_CTRL_RESET; + isp1704_write(isp, ULPI_FUNC_CTRL, r); + usleep_range(1000, 2000); + + /* Set normal mode */ + r &= ~(ULPI_FUNC_CTRL_RESET | ULPI_FUNC_CTRL_OPMODE_MASK); + isp1704_write(isp, ULPI_FUNC_CTRL, r); + + /* Clear the DP and DM pull-down bits */ + r = ULPI_OTG_CTRL_DP_PULLDOWN | ULPI_OTG_CTRL_DM_PULLDOWN; + isp1704_write(isp, ULPI_CLR(ULPI_OTG_CTRL), r); + + /* Enable strong pull-up on DP (1.5K) and reset */ + r = ULPI_FUNC_CTRL_TERMSELECT | ULPI_FUNC_CTRL_RESET; + isp1704_write(isp, ULPI_SET(ULPI_FUNC_CTRL), r); + usleep_range(1000, 2000); + + /* Read the line state */ + if (!isp1704_read(isp, ULPI_DEBUG)) { + /* Disable strong pull-up on DP (1.5K) */ + isp1704_write(isp, ULPI_CLR(ULPI_FUNC_CTRL), + ULPI_FUNC_CTRL_TERMSELECT); + return 1; + } + + /* Is it a charger or PS/2 connection */ + + /* Enable weak pull-up resistor on DP */ + isp1704_write(isp, ULPI_SET(ISP1704_PWR_CTRL), + ISP1704_PWR_CTRL_DP_WKPU_EN); + + /* Disable strong pull-up on DP (1.5K) */ + isp1704_write(isp, ULPI_CLR(ULPI_FUNC_CTRL), + ULPI_FUNC_CTRL_TERMSELECT); + + /* Enable weak pull-down resistor on DM */ + isp1704_write(isp, ULPI_SET(ULPI_OTG_CTRL), + ULPI_OTG_CTRL_DM_PULLDOWN); + + /* It's a charger if the line states are clear */ + if (!(isp1704_read(isp, ULPI_DEBUG))) + ret = 1; + + /* Disable weak pull-up resistor on DP */ + isp1704_write(isp, ULPI_CLR(ISP1704_PWR_CTRL), + ISP1704_PWR_CTRL_DP_WKPU_EN); + + return ret; +} + +static inline int isp1704_charger_detect(struct isp1704_charger *isp) +{ + unsigned long timeout; + u8 pwr_ctrl; + int ret = 0; + + pwr_ctrl = isp1704_read(isp, ISP1704_PWR_CTRL); + + /* set SW control bit in PWR_CTRL register */ + isp1704_write(isp, ISP1704_PWR_CTRL, + ISP1704_PWR_CTRL_SWCTRL); + + /* enable manual charger detection */ + isp1704_write(isp, ULPI_SET(ISP1704_PWR_CTRL), + ISP1704_PWR_CTRL_SWCTRL + | ISP1704_PWR_CTRL_DPVSRC_EN); + usleep_range(1000, 2000); + + timeout = jiffies + msecs_to_jiffies(300); + do { + /* Check if there is a charger */ + if (isp1704_read(isp, ISP1704_PWR_CTRL) + & ISP1704_PWR_CTRL_VDAT_DET) { + ret = isp1704_charger_verify(isp); + break; + } + } while (!time_after(jiffies, timeout) && isp->online); + + /* recover original state */ + isp1704_write(isp, ISP1704_PWR_CTRL, pwr_ctrl); + + return ret; +} + +static inline int isp1704_charger_detect_dcp(struct isp1704_charger *isp) +{ + if (isp1704_charger_detect(isp) && + isp1704_charger_type(isp) == POWER_SUPPLY_TYPE_USB_DCP) + return true; + else + return false; +} + +static void isp1704_charger_work(struct work_struct *data) +{ + struct isp1704_charger *isp = + container_of(data, struct isp1704_charger, work); + static DEFINE_MUTEX(lock); + + mutex_lock(&lock); + + switch (isp->phy->last_event) { + case USB_EVENT_VBUS: + /* do not call wall charger detection more times */ + if (!isp->present) { + isp->online = true; + isp->present = 1; + isp1704_charger_set_power(isp, 1); + + /* detect wall charger */ + if (isp1704_charger_detect_dcp(isp)) { + isp->psy_desc.type = POWER_SUPPLY_TYPE_USB_DCP; + isp->current_max = 1800; + } else { + isp->psy_desc.type = POWER_SUPPLY_TYPE_USB; + isp->current_max = 500; + } + + /* enable data pullups */ + if (isp->phy->otg->gadget) + usb_gadget_connect(isp->phy->otg->gadget); + } + + if (isp->psy_desc.type != POWER_SUPPLY_TYPE_USB_DCP) { + /* + * Only 500mA here or high speed chirp + * handshaking may break + */ + if (isp->current_max > 500) + isp->current_max = 500; + + if (isp->current_max > 100) + isp->psy_desc.type = POWER_SUPPLY_TYPE_USB_CDP; + } + break; + case USB_EVENT_NONE: + isp->online = false; + isp->present = 0; + isp->current_max = 0; + isp->psy_desc.type = POWER_SUPPLY_TYPE_USB; + + /* + * Disable data pullups. We need to prevent the controller from + * enumerating. + * + * FIXME: This is here to allow charger detection with Host/HUB + * chargers. The pullups may be enabled elsewhere, so this can + * not be the final solution. + */ + if (isp->phy->otg->gadget) + usb_gadget_disconnect(isp->phy->otg->gadget); + + isp1704_charger_set_power(isp, 0); + break; + default: + goto out; + } + + power_supply_changed(isp->psy); +out: + mutex_unlock(&lock); +} + +static int isp1704_notifier_call(struct notifier_block *nb, + unsigned long val, void *v) +{ + struct isp1704_charger *isp = + container_of(nb, struct isp1704_charger, nb); + + schedule_work(&isp->work); + + return NOTIFY_OK; +} + +static int isp1704_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct isp1704_charger *isp = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + val->intval = isp->present; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = isp->online; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = isp->current_max; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = isp->model; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = "NXP"; + break; + default: + return -EINVAL; + } + return 0; +} + +static enum power_supply_property power_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static inline int isp1704_test_ulpi(struct isp1704_charger *isp) +{ + int vendor; + int product; + int i; + int ret; + + /* Test ULPI interface */ + ret = isp1704_write(isp, ULPI_SCRATCH, 0xaa); + if (ret < 0) + return ret; + + ret = isp1704_read(isp, ULPI_SCRATCH); + if (ret < 0) + return ret; + + if (ret != 0xaa) + return -ENODEV; + + /* Verify the product and vendor id matches */ + vendor = isp1704_read(isp, ULPI_VENDOR_ID_LOW); + vendor |= isp1704_read(isp, ULPI_VENDOR_ID_HIGH) << 8; + if (vendor != NXP_VENDOR_ID) + return -ENODEV; + + product = isp1704_read(isp, ULPI_PRODUCT_ID_LOW); + product |= isp1704_read(isp, ULPI_PRODUCT_ID_HIGH) << 8; + + for (i = 0; i < ARRAY_SIZE(isp170x_id); i++) { + if (product == isp170x_id[i]) { + sprintf(isp->model, "isp%x", product); + return product; + } + } + + dev_err(isp->dev, "product id %x not matching known ids", product); + + return -ENODEV; +} + +static int isp1704_charger_probe(struct platform_device *pdev) +{ + struct isp1704_charger *isp; + int ret = -ENODEV; + struct power_supply_config psy_cfg = {}; + + isp = devm_kzalloc(&pdev->dev, sizeof(*isp), GFP_KERNEL); + if (!isp) + return -ENOMEM; + + isp->enable_gpio = devm_gpiod_get(&pdev->dev, "nxp,enable", + GPIOD_OUT_HIGH); + if (IS_ERR(isp->enable_gpio)) { + ret = PTR_ERR(isp->enable_gpio); + dev_err(&pdev->dev, "Could not get reset gpio: %d\n", ret); + return ret; + } + + if (pdev->dev.of_node) + isp->phy = devm_usb_get_phy_by_phandle(&pdev->dev, "usb-phy", 0); + else + isp->phy = devm_usb_get_phy(&pdev->dev, USB_PHY_TYPE_USB2); + + if (IS_ERR(isp->phy)) { + ret = PTR_ERR(isp->phy); + dev_err(&pdev->dev, "usb_get_phy failed\n"); + goto fail0; + } + + isp->dev = &pdev->dev; + platform_set_drvdata(pdev, isp); + + isp1704_charger_set_power(isp, 1); + + ret = isp1704_test_ulpi(isp); + if (ret < 0) { + dev_err(&pdev->dev, "isp1704_test_ulpi failed\n"); + goto fail1; + } + + isp->psy_desc.name = "isp1704"; + isp->psy_desc.type = POWER_SUPPLY_TYPE_USB; + isp->psy_desc.properties = power_props; + isp->psy_desc.num_properties = ARRAY_SIZE(power_props); + isp->psy_desc.get_property = isp1704_charger_get_property; + + psy_cfg.drv_data = isp; + + isp->psy = power_supply_register(isp->dev, &isp->psy_desc, &psy_cfg); + if (IS_ERR(isp->psy)) { + ret = PTR_ERR(isp->psy); + dev_err(&pdev->dev, "power_supply_register failed\n"); + goto fail1; + } + + /* + * REVISIT: using work in order to allow the usb notifications to be + * made atomically in the future. + */ + INIT_WORK(&isp->work, isp1704_charger_work); + + isp->nb.notifier_call = isp1704_notifier_call; + + ret = usb_register_notifier(isp->phy, &isp->nb); + if (ret) { + dev_err(&pdev->dev, "usb_register_notifier failed\n"); + goto fail2; + } + + dev_info(isp->dev, "registered with product id %s\n", isp->model); + + /* + * Taking over the D+ pullup. + * + * FIXME: The device will be disconnected if it was already + * enumerated. The charger driver should be always loaded before any + * gadget is loaded. + */ + if (isp->phy->otg->gadget) + usb_gadget_disconnect(isp->phy->otg->gadget); + + if (isp->phy->last_event == USB_EVENT_NONE) + isp1704_charger_set_power(isp, 0); + + /* Detect charger if VBUS is valid (the cable was already plugged). */ + if (isp->phy->last_event == USB_EVENT_VBUS && + !isp->phy->otg->default_a) + schedule_work(&isp->work); + + return 0; +fail2: + power_supply_unregister(isp->psy); +fail1: + isp1704_charger_set_power(isp, 0); +fail0: + dev_err(&pdev->dev, "failed to register isp1704 with error %d\n", ret); + + return ret; +} + +static int isp1704_charger_remove(struct platform_device *pdev) +{ + struct isp1704_charger *isp = platform_get_drvdata(pdev); + + usb_unregister_notifier(isp->phy, &isp->nb); + power_supply_unregister(isp->psy); + isp1704_charger_set_power(isp, 0); + + return 0; +} + +#ifdef CONFIG_OF +static const struct of_device_id omap_isp1704_of_match[] = { + { .compatible = "nxp,isp1704", }, + { .compatible = "nxp,isp1707", }, + {}, +}; +MODULE_DEVICE_TABLE(of, omap_isp1704_of_match); +#endif + +static struct platform_driver isp1704_charger_driver = { + .driver = { + .name = "isp1704_charger", + .of_match_table = of_match_ptr(omap_isp1704_of_match), + }, + .probe = isp1704_charger_probe, + .remove = isp1704_charger_remove, +}; + +module_platform_driver(isp1704_charger_driver); + +MODULE_ALIAS("platform:isp1704_charger"); +MODULE_AUTHOR("Nokia Corporation"); +MODULE_DESCRIPTION("ISP170x USB Charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/lego_ev3_battery.c b/drivers/power/supply/lego_ev3_battery.c new file mode 100644 index 000000000..ccb00be38 --- /dev/null +++ b/drivers/power/supply/lego_ev3_battery.c @@ -0,0 +1,232 @@ +/* + * Battery driver for LEGO MINDSTORMS EV3 + * + * Copyright (C) 2017 David Lechner <david@lechnology.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/gpio/consumer.h> +#include <linux/iio/consumer.h> +#include <linux/iio/types.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> + +struct lego_ev3_battery { + struct iio_channel *iio_v; + struct iio_channel *iio_i; + struct gpio_desc *rechargeable_gpio; + struct power_supply *psy; + int technology; + int v_max; + int v_min; +}; + +static int lego_ev3_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lego_ev3_battery *batt = power_supply_get_drvdata(psy); + int ret, val2; + + switch (psp) { + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = batt->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + /* battery voltage is iio channel * 2 + Vce of transistor */ + ret = iio_read_channel_processed(batt->iio_v, &val->intval); + if (ret) + return ret; + + val->intval *= 2000; + val->intval += 50000; + + /* plus adjust for shunt resistor drop */ + ret = iio_read_channel_processed(batt->iio_i, &val2); + if (ret) + return ret; + + val2 *= 1000; + val2 /= 15; + val->intval += val2; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = batt->v_max; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = batt->v_min; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + /* battery current is iio channel / 15 / 0.05 ohms */ + ret = iio_read_channel_processed(batt->iio_i, &val->intval); + if (ret) + return ret; + + val->intval *= 20000; + val->intval /= 15; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int lego_ev3_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct lego_ev3_battery *batt = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_TECHNOLOGY: + /* + * Only allow changing technology from Unknown to NiMH. Li-ion + * batteries are automatically detected and should not be + * overridden. Rechargeable AA batteries, on the other hand, + * cannot be automatically detected, and so must be manually + * specified. This should only be set once during system init, + * so there is no mechanism to go back to Unknown. + */ + if (batt->technology != POWER_SUPPLY_TECHNOLOGY_UNKNOWN) + return -EINVAL; + switch (val->intval) { + case POWER_SUPPLY_TECHNOLOGY_NiMH: + batt->technology = POWER_SUPPLY_TECHNOLOGY_NiMH; + batt->v_max = 7800000; + batt->v_min = 5400000; + break; + default: + return -EINVAL; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static int lego_ev3_battery_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + struct lego_ev3_battery *batt = power_supply_get_drvdata(psy); + + return psp == POWER_SUPPLY_PROP_TECHNOLOGY && + batt->technology == POWER_SUPPLY_TECHNOLOGY_UNKNOWN; +} + +static enum power_supply_property lego_ev3_battery_props[] = { + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_SCOPE, +}; + +static const struct power_supply_desc lego_ev3_battery_desc = { + .name = "lego-ev3-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = lego_ev3_battery_props, + .num_properties = ARRAY_SIZE(lego_ev3_battery_props), + .get_property = lego_ev3_battery_get_property, + .set_property = lego_ev3_battery_set_property, + .property_is_writeable = lego_ev3_battery_property_is_writeable, +}; + +static int lego_ev3_battery_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct lego_ev3_battery *batt; + struct power_supply_config psy_cfg = {}; + int err; + + batt = devm_kzalloc(dev, sizeof(*batt), GFP_KERNEL); + if (!batt) + return -ENOMEM; + + platform_set_drvdata(pdev, batt); + + batt->iio_v = devm_iio_channel_get(dev, "voltage"); + err = PTR_ERR_OR_ZERO(batt->iio_v); + if (err) + return dev_err_probe(dev, err, + "Failed to get voltage iio channel\n"); + + batt->iio_i = devm_iio_channel_get(dev, "current"); + err = PTR_ERR_OR_ZERO(batt->iio_i); + if (err) + return dev_err_probe(dev, err, + "Failed to get current iio channel\n"); + + batt->rechargeable_gpio = devm_gpiod_get(dev, "rechargeable", GPIOD_IN); + err = PTR_ERR_OR_ZERO(batt->rechargeable_gpio); + if (err) + return dev_err_probe(dev, err, + "Failed to get rechargeable gpio\n"); + + /* + * The rechargeable battery indication switch cannot be changed without + * removing the battery, so we only need to read it once. + */ + if (gpiod_get_value(batt->rechargeable_gpio)) { + /* 2-cell Li-ion, 7.4V nominal */ + batt->technology = POWER_SUPPLY_TECHNOLOGY_LION; + batt->v_max = 84000000; + batt->v_min = 60000000; + } else { + /* 6x AA Alkaline, 9V nominal */ + batt->technology = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + batt->v_max = 90000000; + batt->v_min = 48000000; + } + + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = batt; + + batt->psy = devm_power_supply_register(dev, &lego_ev3_battery_desc, + &psy_cfg); + err = PTR_ERR_OR_ZERO(batt->psy); + if (err) { + dev_err(dev, "failed to register power supply\n"); + return err; + } + + return 0; +} + +static const struct of_device_id of_lego_ev3_battery_match[] = { + { .compatible = "lego,ev3-battery", }, + { } +}; +MODULE_DEVICE_TABLE(of, of_lego_ev3_battery_match); + +static struct platform_driver lego_ev3_battery_driver = { + .driver = { + .name = "lego-ev3-battery", + .of_match_table = of_lego_ev3_battery_match, + }, + .probe = lego_ev3_battery_probe, +}; +module_platform_driver(lego_ev3_battery_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_DESCRIPTION("LEGO MINDSTORMS EV3 Battery Driver"); diff --git a/drivers/power/supply/lp8727_charger.c b/drivers/power/supply/lp8727_charger.c new file mode 100644 index 000000000..9ee54e397 --- /dev/null +++ b/drivers/power/supply/lp8727_charger.c @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for LP8727 Micro/Mini USB IC with integrated charger + * + * Copyright (C) 2011 Texas Instruments + * Copyright (C) 2011 National Semiconductor + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/i2c.h> +#include <linux/power_supply.h> +#include <linux/platform_data/lp8727.h> +#include <linux/of.h> + +#define LP8788_NUM_INTREGS 2 +#define DEFAULT_DEBOUNCE_MSEC 270 + +/* Registers */ +#define LP8727_CTRL1 0x1 +#define LP8727_CTRL2 0x2 +#define LP8727_SWCTRL 0x3 +#define LP8727_INT1 0x4 +#define LP8727_INT2 0x5 +#define LP8727_STATUS1 0x6 +#define LP8727_STATUS2 0x7 +#define LP8727_CHGCTRL2 0x9 + +/* CTRL1 register */ +#define LP8727_CP_EN BIT(0) +#define LP8727_ADC_EN BIT(1) +#define LP8727_ID200_EN BIT(4) + +/* CTRL2 register */ +#define LP8727_CHGDET_EN BIT(1) +#define LP8727_INT_EN BIT(6) + +/* SWCTRL register */ +#define LP8727_SW_DM1_DM (0x0 << 0) +#define LP8727_SW_DM1_HiZ (0x7 << 0) +#define LP8727_SW_DP2_DP (0x0 << 3) +#define LP8727_SW_DP2_HiZ (0x7 << 3) + +/* INT1 register */ +#define LP8727_IDNO (0xF << 0) +#define LP8727_VBUS BIT(4) + +/* STATUS1 register */ +#define LP8727_CHGSTAT (3 << 4) +#define LP8727_CHPORT BIT(6) +#define LP8727_DCPORT BIT(7) +#define LP8727_STAT_EOC 0x30 + +/* STATUS2 register */ +#define LP8727_TEMP_STAT (3 << 5) +#define LP8727_TEMP_SHIFT 5 + +/* CHGCTRL2 register */ +#define LP8727_ICHG_SHIFT 4 + +enum lp8727_dev_id { + LP8727_ID_NONE, + LP8727_ID_TA, + LP8727_ID_DEDICATED_CHG, + LP8727_ID_USB_CHG, + LP8727_ID_USB_DS, + LP8727_ID_MAX, +}; + +enum lp8727_die_temp { + LP8788_TEMP_75C, + LP8788_TEMP_95C, + LP8788_TEMP_115C, + LP8788_TEMP_135C, +}; + +struct lp8727_psy { + struct power_supply *ac; + struct power_supply *usb; + struct power_supply *batt; +}; + +struct lp8727_chg { + struct device *dev; + struct i2c_client *client; + struct mutex xfer_lock; + struct lp8727_psy *psy; + struct lp8727_platform_data *pdata; + + /* Charger Data */ + enum lp8727_dev_id devid; + struct lp8727_chg_param *chg_param; + + /* Interrupt Handling */ + int irq; + struct delayed_work work; + unsigned long debounce_jiffies; +}; + +static int lp8727_read_bytes(struct lp8727_chg *pchg, u8 reg, u8 *data, u8 len) +{ + s32 ret; + + mutex_lock(&pchg->xfer_lock); + ret = i2c_smbus_read_i2c_block_data(pchg->client, reg, len, data); + mutex_unlock(&pchg->xfer_lock); + + return (ret != len) ? -EIO : 0; +} + +static inline int lp8727_read_byte(struct lp8727_chg *pchg, u8 reg, u8 *data) +{ + return lp8727_read_bytes(pchg, reg, data, 1); +} + +static int lp8727_write_byte(struct lp8727_chg *pchg, u8 reg, u8 data) +{ + int ret; + + mutex_lock(&pchg->xfer_lock); + ret = i2c_smbus_write_byte_data(pchg->client, reg, data); + mutex_unlock(&pchg->xfer_lock); + + return ret; +} + +static bool lp8727_is_charger_attached(const char *name, int id) +{ + if (!strcmp(name, "ac")) + return id == LP8727_ID_TA || id == LP8727_ID_DEDICATED_CHG; + else if (!strcmp(name, "usb")) + return id == LP8727_ID_USB_CHG; + + return id >= LP8727_ID_TA && id <= LP8727_ID_USB_CHG; +} + +static int lp8727_init_device(struct lp8727_chg *pchg) +{ + u8 val; + int ret; + u8 intstat[LP8788_NUM_INTREGS]; + + /* clear interrupts */ + ret = lp8727_read_bytes(pchg, LP8727_INT1, intstat, LP8788_NUM_INTREGS); + if (ret) + return ret; + + val = LP8727_ID200_EN | LP8727_ADC_EN | LP8727_CP_EN; + ret = lp8727_write_byte(pchg, LP8727_CTRL1, val); + if (ret) + return ret; + + val = LP8727_INT_EN | LP8727_CHGDET_EN; + return lp8727_write_byte(pchg, LP8727_CTRL2, val); +} + +static int lp8727_is_dedicated_charger(struct lp8727_chg *pchg) +{ + u8 val; + + lp8727_read_byte(pchg, LP8727_STATUS1, &val); + return val & LP8727_DCPORT; +} + +static int lp8727_is_usb_charger(struct lp8727_chg *pchg) +{ + u8 val; + + lp8727_read_byte(pchg, LP8727_STATUS1, &val); + return val & LP8727_CHPORT; +} + +static inline void lp8727_ctrl_switch(struct lp8727_chg *pchg, u8 sw) +{ + lp8727_write_byte(pchg, LP8727_SWCTRL, sw); +} + +static void lp8727_id_detection(struct lp8727_chg *pchg, u8 id, int vbusin) +{ + struct lp8727_platform_data *pdata = pchg->pdata; + u8 devid = LP8727_ID_NONE; + u8 swctrl = LP8727_SW_DM1_HiZ | LP8727_SW_DP2_HiZ; + + switch (id) { + case 0x5: + devid = LP8727_ID_TA; + pchg->chg_param = pdata ? pdata->ac : NULL; + break; + case 0xB: + if (lp8727_is_dedicated_charger(pchg)) { + pchg->chg_param = pdata ? pdata->ac : NULL; + devid = LP8727_ID_DEDICATED_CHG; + } else if (lp8727_is_usb_charger(pchg)) { + pchg->chg_param = pdata ? pdata->usb : NULL; + devid = LP8727_ID_USB_CHG; + swctrl = LP8727_SW_DM1_DM | LP8727_SW_DP2_DP; + } else if (vbusin) { + devid = LP8727_ID_USB_DS; + swctrl = LP8727_SW_DM1_DM | LP8727_SW_DP2_DP; + } + break; + default: + devid = LP8727_ID_NONE; + pchg->chg_param = NULL; + break; + } + + pchg->devid = devid; + lp8727_ctrl_switch(pchg, swctrl); +} + +static void lp8727_enable_chgdet(struct lp8727_chg *pchg) +{ + u8 val; + + lp8727_read_byte(pchg, LP8727_CTRL2, &val); + val |= LP8727_CHGDET_EN; + lp8727_write_byte(pchg, LP8727_CTRL2, val); +} + +static void lp8727_delayed_func(struct work_struct *_work) +{ + struct lp8727_chg *pchg = container_of(_work, struct lp8727_chg, + work.work); + u8 intstat[LP8788_NUM_INTREGS]; + u8 idno; + u8 vbus; + + if (lp8727_read_bytes(pchg, LP8727_INT1, intstat, LP8788_NUM_INTREGS)) { + dev_err(pchg->dev, "can not read INT registers\n"); + return; + } + + idno = intstat[0] & LP8727_IDNO; + vbus = intstat[0] & LP8727_VBUS; + + lp8727_id_detection(pchg, idno, vbus); + lp8727_enable_chgdet(pchg); + + power_supply_changed(pchg->psy->ac); + power_supply_changed(pchg->psy->usb); + power_supply_changed(pchg->psy->batt); +} + +static irqreturn_t lp8727_isr_func(int irq, void *ptr) +{ + struct lp8727_chg *pchg = ptr; + + schedule_delayed_work(&pchg->work, pchg->debounce_jiffies); + return IRQ_HANDLED; +} + +static int lp8727_setup_irq(struct lp8727_chg *pchg) +{ + int ret; + int irq = pchg->client->irq; + unsigned delay_msec = pchg->pdata ? pchg->pdata->debounce_msec : + DEFAULT_DEBOUNCE_MSEC; + + INIT_DELAYED_WORK(&pchg->work, lp8727_delayed_func); + + if (irq <= 0) { + dev_warn(pchg->dev, "invalid irq number: %d\n", irq); + return 0; + } + + ret = request_threaded_irq(irq, NULL, lp8727_isr_func, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + "lp8727_irq", pchg); + + if (ret) + return ret; + + pchg->irq = irq; + pchg->debounce_jiffies = msecs_to_jiffies(delay_msec); + + return 0; +} + +static void lp8727_release_irq(struct lp8727_chg *pchg) +{ + cancel_delayed_work_sync(&pchg->work); + + if (pchg->irq) + free_irq(pchg->irq, pchg); +} + +static enum power_supply_property lp8727_charger_prop[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static enum power_supply_property lp8727_battery_prop[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TEMP, +}; + +static char *battery_supplied_to[] = { + "main_batt", +}; + +static int lp8727_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lp8727_chg *pchg = dev_get_drvdata(psy->dev.parent); + + if (psp != POWER_SUPPLY_PROP_ONLINE) + return -EINVAL; + + val->intval = lp8727_is_charger_attached(psy->desc->name, pchg->devid); + + return 0; +} + +static bool lp8727_is_high_temperature(enum lp8727_die_temp temp) +{ + switch (temp) { + case LP8788_TEMP_95C: + case LP8788_TEMP_115C: + case LP8788_TEMP_135C: + return true; + default: + return false; + } +} + +static int lp8727_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lp8727_chg *pchg = dev_get_drvdata(psy->dev.parent); + struct lp8727_platform_data *pdata = pchg->pdata; + enum lp8727_die_temp temp; + u8 read; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (!lp8727_is_charger_attached(psy->desc->name, pchg->devid)) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + lp8727_read_byte(pchg, LP8727_STATUS1, &read); + + val->intval = (read & LP8727_CHGSTAT) == LP8727_STAT_EOC ? + POWER_SUPPLY_STATUS_FULL : + POWER_SUPPLY_STATUS_CHARGING; + break; + case POWER_SUPPLY_PROP_HEALTH: + lp8727_read_byte(pchg, LP8727_STATUS2, &read); + temp = (read & LP8727_TEMP_STAT) >> LP8727_TEMP_SHIFT; + + val->intval = lp8727_is_high_temperature(temp) ? + POWER_SUPPLY_HEALTH_OVERHEAT : + POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_PRESENT: + if (!pdata) + return -EINVAL; + + if (pdata->get_batt_present) + val->intval = pdata->get_batt_present(); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (!pdata) + return -EINVAL; + + if (pdata->get_batt_level) + val->intval = pdata->get_batt_level(); + break; + case POWER_SUPPLY_PROP_CAPACITY: + if (!pdata) + return -EINVAL; + + if (pdata->get_batt_capacity) + val->intval = pdata->get_batt_capacity(); + break; + case POWER_SUPPLY_PROP_TEMP: + if (!pdata) + return -EINVAL; + + if (pdata->get_batt_temp) + val->intval = pdata->get_batt_temp(); + break; + default: + break; + } + + return 0; +} + +static void lp8727_charger_changed(struct power_supply *psy) +{ + struct lp8727_chg *pchg = dev_get_drvdata(psy->dev.parent); + u8 eoc_level; + u8 ichg; + u8 val; + + /* skip if no charger exists */ + if (!lp8727_is_charger_attached(psy->desc->name, pchg->devid)) + return; + + /* update charging parameters */ + if (pchg->chg_param) { + eoc_level = pchg->chg_param->eoc_level; + ichg = pchg->chg_param->ichg; + val = (ichg << LP8727_ICHG_SHIFT) | eoc_level; + lp8727_write_byte(pchg, LP8727_CHGCTRL2, val); + } +} + +static const struct power_supply_desc lp8727_ac_desc = { + .name = "ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = lp8727_charger_prop, + .num_properties = ARRAY_SIZE(lp8727_charger_prop), + .get_property = lp8727_charger_get_property, +}; + +static const struct power_supply_desc lp8727_usb_desc = { + .name = "usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = lp8727_charger_prop, + .num_properties = ARRAY_SIZE(lp8727_charger_prop), + .get_property = lp8727_charger_get_property, +}; + +static const struct power_supply_desc lp8727_batt_desc = { + .name = "main_batt", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = lp8727_battery_prop, + .num_properties = ARRAY_SIZE(lp8727_battery_prop), + .get_property = lp8727_battery_get_property, + .external_power_changed = lp8727_charger_changed, +}; + +static int lp8727_register_psy(struct lp8727_chg *pchg) +{ + struct power_supply_config psy_cfg = {}; /* Only for ac and usb */ + struct lp8727_psy *psy; + + psy = devm_kzalloc(pchg->dev, sizeof(*psy), GFP_KERNEL); + if (!psy) + return -ENOMEM; + + pchg->psy = psy; + + psy_cfg.supplied_to = battery_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(battery_supplied_to); + + psy->ac = power_supply_register(pchg->dev, &lp8727_ac_desc, &psy_cfg); + if (IS_ERR(psy->ac)) + goto err_psy_ac; + + psy->usb = power_supply_register(pchg->dev, &lp8727_usb_desc, + &psy_cfg); + if (IS_ERR(psy->usb)) + goto err_psy_usb; + + psy->batt = power_supply_register(pchg->dev, &lp8727_batt_desc, NULL); + if (IS_ERR(psy->batt)) + goto err_psy_batt; + + return 0; + +err_psy_batt: + power_supply_unregister(psy->usb); +err_psy_usb: + power_supply_unregister(psy->ac); +err_psy_ac: + return -EPERM; +} + +static void lp8727_unregister_psy(struct lp8727_chg *pchg) +{ + struct lp8727_psy *psy = pchg->psy; + + if (!psy) + return; + + power_supply_unregister(psy->ac); + power_supply_unregister(psy->usb); + power_supply_unregister(psy->batt); +} + +#ifdef CONFIG_OF +static struct lp8727_chg_param +*lp8727_parse_charge_pdata(struct device *dev, struct device_node *np) +{ + struct lp8727_chg_param *param; + + param = devm_kzalloc(dev, sizeof(*param), GFP_KERNEL); + if (!param) + goto out; + + of_property_read_u8(np, "eoc-level", (u8 *)¶m->eoc_level); + of_property_read_u8(np, "charging-current", (u8 *)¶m->ichg); +out: + return param; +} + +static struct lp8727_platform_data *lp8727_parse_dt(struct device *dev) +{ + struct device_node *np = dev->of_node; + struct device_node *child; + struct lp8727_platform_data *pdata; + const char *type; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + of_property_read_u32(np, "debounce-ms", &pdata->debounce_msec); + + /* If charging parameter is not defined, just skip parsing the dt */ + if (of_get_child_count(np) == 0) + return pdata; + + for_each_child_of_node(np, child) { + of_property_read_string(child, "charger-type", &type); + + if (!strcmp(type, "ac")) + pdata->ac = lp8727_parse_charge_pdata(dev, child); + + if (!strcmp(type, "usb")) + pdata->usb = lp8727_parse_charge_pdata(dev, child); + } + + return pdata; +} +#else +static struct lp8727_platform_data *lp8727_parse_dt(struct device *dev) +{ + return NULL; +} +#endif + +static int lp8727_probe(struct i2c_client *cl, const struct i2c_device_id *id) +{ + struct lp8727_chg *pchg; + struct lp8727_platform_data *pdata; + int ret; + + if (!i2c_check_functionality(cl->adapter, I2C_FUNC_SMBUS_I2C_BLOCK)) + return -EIO; + + if (cl->dev.of_node) { + pdata = lp8727_parse_dt(&cl->dev); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else { + pdata = dev_get_platdata(&cl->dev); + } + + pchg = devm_kzalloc(&cl->dev, sizeof(*pchg), GFP_KERNEL); + if (!pchg) + return -ENOMEM; + + pchg->client = cl; + pchg->dev = &cl->dev; + pchg->pdata = pdata; + i2c_set_clientdata(cl, pchg); + + mutex_init(&pchg->xfer_lock); + + ret = lp8727_init_device(pchg); + if (ret) { + dev_err(pchg->dev, "i2c communication err: %d", ret); + return ret; + } + + ret = lp8727_register_psy(pchg); + if (ret) { + dev_err(pchg->dev, "power supplies register err: %d", ret); + return ret; + } + + ret = lp8727_setup_irq(pchg); + if (ret) { + dev_err(pchg->dev, "irq handler err: %d", ret); + lp8727_unregister_psy(pchg); + return ret; + } + + return 0; +} + +static int lp8727_remove(struct i2c_client *cl) +{ + struct lp8727_chg *pchg = i2c_get_clientdata(cl); + + lp8727_release_irq(pchg); + lp8727_unregister_psy(pchg); + return 0; +} + +static const struct of_device_id lp8727_dt_ids[] = { + { .compatible = "ti,lp8727", }, + { } +}; +MODULE_DEVICE_TABLE(of, lp8727_dt_ids); + +static const struct i2c_device_id lp8727_ids[] = { + {"lp8727", 0}, + { } +}; +MODULE_DEVICE_TABLE(i2c, lp8727_ids); + +static struct i2c_driver lp8727_driver = { + .driver = { + .name = "lp8727", + .of_match_table = of_match_ptr(lp8727_dt_ids), + }, + .probe = lp8727_probe, + .remove = lp8727_remove, + .id_table = lp8727_ids, +}; +module_i2c_driver(lp8727_driver); + +MODULE_DESCRIPTION("TI/National Semiconductor LP8727 charger driver"); +MODULE_AUTHOR("Milo Kim <milo.kim@ti.com>, Daniel Jeong <daniel.jeong@ti.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/lp8788-charger.c b/drivers/power/supply/lp8788-charger.c new file mode 100644 index 000000000..397e5a03b --- /dev/null +++ b/drivers/power/supply/lp8788-charger.c @@ -0,0 +1,741 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TI LP8788 MFD - battery charger driver + * + * Copyright 2012 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/err.h> +#include <linux/iio/consumer.h> +#include <linux/interrupt.h> +#include <linux/irqdomain.h> +#include <linux/mfd/lp8788.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +/* register address */ +#define LP8788_CHG_STATUS 0x07 +#define LP8788_CHG_IDCIN 0x13 +#define LP8788_CHG_IBATT 0x14 +#define LP8788_CHG_VTERM 0x15 +#define LP8788_CHG_EOC 0x16 + +/* mask/shift bits */ +#define LP8788_CHG_INPUT_STATE_M 0x03 /* Addr 07h */ +#define LP8788_CHG_STATE_M 0x3C +#define LP8788_CHG_STATE_S 2 +#define LP8788_NO_BATT_M BIT(6) +#define LP8788_BAD_BATT_M BIT(7) +#define LP8788_CHG_IBATT_M 0x1F /* Addr 14h */ +#define LP8788_CHG_VTERM_M 0x0F /* Addr 15h */ +#define LP8788_CHG_EOC_LEVEL_M 0x30 /* Addr 16h */ +#define LP8788_CHG_EOC_LEVEL_S 4 +#define LP8788_CHG_EOC_TIME_M 0x0E +#define LP8788_CHG_EOC_TIME_S 1 +#define LP8788_CHG_EOC_MODE_M BIT(0) + +#define LP8788_CHARGER_NAME "charger" +#define LP8788_BATTERY_NAME "main_batt" + +#define LP8788_CHG_START 0x11 +#define LP8788_CHG_END 0x1C + +#define LP8788_ISEL_MAX 23 +#define LP8788_ISEL_STEP 50 +#define LP8788_VTERM_MIN 4100 +#define LP8788_VTERM_STEP 25 +#define LP8788_MAX_BATT_CAPACITY 100 +#define LP8788_MAX_CHG_IRQS 11 + +enum lp8788_charging_state { + LP8788_OFF, + LP8788_WARM_UP, + LP8788_LOW_INPUT = 0x3, + LP8788_PRECHARGE, + LP8788_CC, + LP8788_CV, + LP8788_MAINTENANCE, + LP8788_BATTERY_FAULT, + LP8788_SYSTEM_SUPPORT = 0xC, + LP8788_HIGH_CURRENT = 0xF, + LP8788_MAX_CHG_STATE, +}; + +enum lp8788_charger_adc_sel { + LP8788_VBATT, + LP8788_BATT_TEMP, + LP8788_NUM_CHG_ADC, +}; + +enum lp8788_charger_input_state { + LP8788_SYSTEM_SUPPLY = 1, + LP8788_FULL_FUNCTION, +}; + +/* + * struct lp8788_chg_irq + * @which : lp8788 interrupt id + * @virq : Linux IRQ number from irq_domain + */ +struct lp8788_chg_irq { + enum lp8788_int_id which; + int virq; +}; + +/* + * struct lp8788_charger + * @lp : used for accessing the registers of mfd lp8788 device + * @charger : power supply driver for the battery charger + * @battery : power supply driver for the battery + * @charger_work : work queue for charger input interrupts + * @chan : iio channels for getting adc values + * eg) battery voltage, capacity and temperature + * @irqs : charger dedicated interrupts + * @num_irqs : total numbers of charger interrupts + * @pdata : charger platform specific data + */ +struct lp8788_charger { + struct lp8788 *lp; + struct power_supply *charger; + struct power_supply *battery; + struct work_struct charger_work; + struct iio_channel *chan[LP8788_NUM_CHG_ADC]; + struct lp8788_chg_irq irqs[LP8788_MAX_CHG_IRQS]; + int num_irqs; + struct lp8788_charger_platform_data *pdata; +}; + +static char *battery_supplied_to[] = { + LP8788_BATTERY_NAME, +}; + +static enum power_supply_property lp8788_charger_prop[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static enum power_supply_property lp8788_battery_prop[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, + POWER_SUPPLY_PROP_TEMP, +}; + +static bool lp8788_is_charger_detected(struct lp8788_charger *pchg) +{ + u8 data; + + lp8788_read_byte(pchg->lp, LP8788_CHG_STATUS, &data); + data &= LP8788_CHG_INPUT_STATE_M; + + return data == LP8788_SYSTEM_SUPPLY || data == LP8788_FULL_FUNCTION; +} + +static int lp8788_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lp8788_charger *pchg = dev_get_drvdata(psy->dev.parent); + u8 read; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = lp8788_is_charger_detected(pchg); + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + lp8788_read_byte(pchg->lp, LP8788_CHG_IDCIN, &read); + val->intval = LP8788_ISEL_STEP * + (min_t(int, read, LP8788_ISEL_MAX) + 1); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int lp8788_get_battery_status(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + enum lp8788_charging_state state; + u8 data; + int ret; + + ret = lp8788_read_byte(pchg->lp, LP8788_CHG_STATUS, &data); + if (ret) + return ret; + + state = (data & LP8788_CHG_STATE_M) >> LP8788_CHG_STATE_S; + switch (state) { + case LP8788_OFF: + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case LP8788_PRECHARGE: + case LP8788_CC: + case LP8788_CV: + case LP8788_HIGH_CURRENT: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case LP8788_MAINTENANCE: + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + default: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + } + + return 0; +} + +static int lp8788_get_battery_health(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + u8 data; + int ret; + + ret = lp8788_read_byte(pchg->lp, LP8788_CHG_STATUS, &data); + if (ret) + return ret; + + if (data & LP8788_NO_BATT_M) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else if (data & LP8788_BAD_BATT_M) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + + return 0; +} + +static int lp8788_get_battery_present(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + u8 data; + int ret; + + ret = lp8788_read_byte(pchg->lp, LP8788_CHG_STATUS, &data); + if (ret) + return ret; + + val->intval = !(data & LP8788_NO_BATT_M); + return 0; +} + +static int lp8788_get_vbatt_adc(struct lp8788_charger *pchg, int *result) +{ + struct iio_channel *channel = pchg->chan[LP8788_VBATT]; + + if (!channel) + return -EINVAL; + + return iio_read_channel_processed(channel, result); +} + +static int lp8788_get_battery_voltage(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + return lp8788_get_vbatt_adc(pchg, &val->intval); +} + +static int lp8788_get_battery_capacity(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + struct lp8788 *lp = pchg->lp; + struct lp8788_charger_platform_data *pdata = pchg->pdata; + unsigned int max_vbatt; + int vbatt; + enum lp8788_charging_state state; + u8 data; + int ret; + + if (!pdata) + return -EINVAL; + + max_vbatt = pdata->max_vbatt_mv; + if (max_vbatt == 0) + return -EINVAL; + + ret = lp8788_read_byte(lp, LP8788_CHG_STATUS, &data); + if (ret) + return ret; + + state = (data & LP8788_CHG_STATE_M) >> LP8788_CHG_STATE_S; + + if (state == LP8788_MAINTENANCE) { + val->intval = LP8788_MAX_BATT_CAPACITY; + } else { + ret = lp8788_get_vbatt_adc(pchg, &vbatt); + if (ret) + return ret; + + val->intval = (vbatt * LP8788_MAX_BATT_CAPACITY) / max_vbatt; + val->intval = min(val->intval, LP8788_MAX_BATT_CAPACITY); + } + + return 0; +} + +static int lp8788_get_battery_temperature(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + struct iio_channel *channel = pchg->chan[LP8788_BATT_TEMP]; + int result; + int ret; + + if (!channel) + return -EINVAL; + + ret = iio_read_channel_processed(channel, &result); + if (ret < 0) + return -EINVAL; + + /* unit: 0.1 'C */ + val->intval = result * 10; + + return 0; +} + +static int lp8788_get_battery_charging_current(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + u8 read; + + lp8788_read_byte(pchg->lp, LP8788_CHG_IBATT, &read); + read &= LP8788_CHG_IBATT_M; + val->intval = LP8788_ISEL_STEP * + (min_t(int, read, LP8788_ISEL_MAX) + 1); + + return 0; +} + +static int lp8788_get_charging_termination_voltage(struct lp8788_charger *pchg, + union power_supply_propval *val) +{ + u8 read; + + lp8788_read_byte(pchg->lp, LP8788_CHG_VTERM, &read); + read &= LP8788_CHG_VTERM_M; + val->intval = LP8788_VTERM_MIN + LP8788_VTERM_STEP * read; + + return 0; +} + +static int lp8788_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct lp8788_charger *pchg = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + return lp8788_get_battery_status(pchg, val); + case POWER_SUPPLY_PROP_HEALTH: + return lp8788_get_battery_health(pchg, val); + case POWER_SUPPLY_PROP_PRESENT: + return lp8788_get_battery_present(pchg, val); + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return lp8788_get_battery_voltage(pchg, val); + case POWER_SUPPLY_PROP_CAPACITY: + return lp8788_get_battery_capacity(pchg, val); + case POWER_SUPPLY_PROP_TEMP: + return lp8788_get_battery_temperature(pchg, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return lp8788_get_battery_charging_current(pchg, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + return lp8788_get_charging_termination_voltage(pchg, val); + default: + return -EINVAL; + } +} + +static inline bool lp8788_is_valid_charger_register(u8 addr) +{ + return addr >= LP8788_CHG_START && addr <= LP8788_CHG_END; +} + +static int lp8788_update_charger_params(struct platform_device *pdev, + struct lp8788_charger *pchg) +{ + struct lp8788 *lp = pchg->lp; + struct lp8788_charger_platform_data *pdata = pchg->pdata; + struct lp8788_chg_param *param; + int i; + int ret; + + if (!pdata || !pdata->chg_params) { + dev_info(&pdev->dev, "skip updating charger parameters\n"); + return 0; + } + + /* settting charging parameters */ + for (i = 0; i < pdata->num_chg_params; i++) { + param = pdata->chg_params + i; + + if (lp8788_is_valid_charger_register(param->addr)) { + ret = lp8788_write_byte(lp, param->addr, param->val); + if (ret) + return ret; + } + } + + return 0; +} + +static const struct power_supply_desc lp8788_psy_charger_desc = { + .name = LP8788_CHARGER_NAME, + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = lp8788_charger_prop, + .num_properties = ARRAY_SIZE(lp8788_charger_prop), + .get_property = lp8788_charger_get_property, +}; + +static const struct power_supply_desc lp8788_psy_battery_desc = { + .name = LP8788_BATTERY_NAME, + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = lp8788_battery_prop, + .num_properties = ARRAY_SIZE(lp8788_battery_prop), + .get_property = lp8788_battery_get_property, +}; + +static void lp8788_psy_unregister(struct lp8788_charger *pchg) +{ + power_supply_unregister(pchg->battery); + power_supply_unregister(pchg->charger); +} + +static void lp8788_charger_event(struct work_struct *work) +{ + struct lp8788_charger *pchg = + container_of(work, struct lp8788_charger, charger_work); + struct lp8788_charger_platform_data *pdata = pchg->pdata; + enum lp8788_charger_event event = lp8788_is_charger_detected(pchg); + + pdata->charger_event(pchg->lp, event); +} + +static bool lp8788_find_irq_id(struct lp8788_charger *pchg, int virq, int *id) +{ + bool found = false; + int i; + + for (i = 0; i < pchg->num_irqs; i++) { + if (pchg->irqs[i].virq == virq) { + *id = pchg->irqs[i].which; + found = true; + break; + } + } + + return found; +} + +static irqreturn_t lp8788_charger_irq_thread(int virq, void *ptr) +{ + struct lp8788_charger *pchg = ptr; + struct lp8788_charger_platform_data *pdata = pchg->pdata; + int id = -1; + + if (!lp8788_find_irq_id(pchg, virq, &id)) + return IRQ_NONE; + + switch (id) { + case LP8788_INT_CHG_INPUT_STATE: + case LP8788_INT_CHG_STATE: + case LP8788_INT_EOC: + case LP8788_INT_BATT_LOW: + case LP8788_INT_NO_BATT: + power_supply_changed(pchg->charger); + power_supply_changed(pchg->battery); + break; + default: + break; + } + + /* report charger dectection event if used */ + if (!pdata) + goto irq_handled; + + if (pdata->charger_event && id == LP8788_INT_CHG_INPUT_STATE) + schedule_work(&pchg->charger_work); + +irq_handled: + return IRQ_HANDLED; +} + +static int lp8788_set_irqs(struct platform_device *pdev, + struct lp8788_charger *pchg, const char *name) +{ + struct resource *r; + struct irq_domain *irqdm = pchg->lp->irqdm; + int irq_start; + int irq_end; + int virq; + int nr_irq; + int i; + int ret; + + /* no error even if no irq resource */ + r = platform_get_resource_byname(pdev, IORESOURCE_IRQ, name); + if (!r) + return 0; + + irq_start = r->start; + irq_end = r->end; + + for (i = irq_start; i <= irq_end; i++) { + nr_irq = pchg->num_irqs; + + virq = irq_create_mapping(irqdm, i); + pchg->irqs[nr_irq].virq = virq; + pchg->irqs[nr_irq].which = i; + pchg->num_irqs++; + + ret = request_threaded_irq(virq, NULL, + lp8788_charger_irq_thread, + IRQF_ONESHOT, name, pchg); + if (ret) + break; + } + + if (i <= irq_end) + goto err_free_irq; + + return 0; + +err_free_irq: + for (i = 0; i < pchg->num_irqs; i++) + free_irq(pchg->irqs[i].virq, pchg); + return ret; +} + +static int lp8788_irq_register(struct platform_device *pdev, + struct lp8788_charger *pchg) +{ + const char *name[] = { + LP8788_CHG_IRQ, LP8788_PRSW_IRQ, LP8788_BATT_IRQ + }; + int i; + int ret; + + INIT_WORK(&pchg->charger_work, lp8788_charger_event); + pchg->num_irqs = 0; + + for (i = 0; i < ARRAY_SIZE(name); i++) { + ret = lp8788_set_irqs(pdev, pchg, name[i]); + if (ret) { + dev_warn(&pdev->dev, "irq setup failed: %s\n", name[i]); + return ret; + } + } + + if (pchg->num_irqs > LP8788_MAX_CHG_IRQS) { + dev_err(&pdev->dev, "invalid total number of irqs: %d\n", + pchg->num_irqs); + return -EINVAL; + } + + + return 0; +} + +static void lp8788_irq_unregister(struct platform_device *pdev, + struct lp8788_charger *pchg) +{ + int i; + int irq; + + for (i = 0; i < pchg->num_irqs; i++) { + irq = pchg->irqs[i].virq; + if (!irq) + continue; + + free_irq(irq, pchg); + } +} + +static void lp8788_setup_adc_channel(struct device *dev, + struct lp8788_charger *pchg) +{ + struct lp8788_charger_platform_data *pdata = pchg->pdata; + struct iio_channel *chan; + + if (!pdata) + return; + + /* ADC channel for battery voltage */ + chan = devm_iio_channel_get(dev, pdata->adc_vbatt); + pchg->chan[LP8788_VBATT] = IS_ERR(chan) ? NULL : chan; + + /* ADC channel for battery temperature */ + chan = devm_iio_channel_get(dev, pdata->adc_batt_temp); + pchg->chan[LP8788_BATT_TEMP] = IS_ERR(chan) ? NULL : chan; +} + +static ssize_t lp8788_show_charger_status(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct lp8788_charger *pchg = dev_get_drvdata(dev); + enum lp8788_charging_state state; + static const char * const desc[LP8788_MAX_CHG_STATE] = { + [LP8788_OFF] = "CHARGER OFF", + [LP8788_WARM_UP] = "WARM UP", + [LP8788_LOW_INPUT] = "LOW INPUT STATE", + [LP8788_PRECHARGE] = "CHARGING - PRECHARGE", + [LP8788_CC] = "CHARGING - CC", + [LP8788_CV] = "CHARGING - CV", + [LP8788_MAINTENANCE] = "NO CHARGING - MAINTENANCE", + [LP8788_BATTERY_FAULT] = "BATTERY FAULT", + [LP8788_SYSTEM_SUPPORT] = "SYSTEM SUPPORT", + [LP8788_HIGH_CURRENT] = "HIGH CURRENT", + }; + u8 data; + + lp8788_read_byte(pchg->lp, LP8788_CHG_STATUS, &data); + state = (data & LP8788_CHG_STATE_M) >> LP8788_CHG_STATE_S; + + return scnprintf(buf, PAGE_SIZE, "%s\n", desc[state]); +} + +static ssize_t lp8788_show_eoc_time(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct lp8788_charger *pchg = dev_get_drvdata(dev); + static const char * const stime[] = { + "400ms", "5min", "10min", "15min", + "20min", "25min", "30min", "No timeout" + }; + u8 val; + + lp8788_read_byte(pchg->lp, LP8788_CHG_EOC, &val); + val = (val & LP8788_CHG_EOC_TIME_M) >> LP8788_CHG_EOC_TIME_S; + + return scnprintf(buf, PAGE_SIZE, "End Of Charge Time: %s\n", + stime[val]); +} + +static ssize_t lp8788_show_eoc_level(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct lp8788_charger *pchg = dev_get_drvdata(dev); + static const char * const abs_level[] = { + "25mA", "49mA", "75mA", "98mA" + }; + static const char * const relative_level[] = { + "5%", "10%", "15%", "20%" + }; + const char *level; + u8 val; + u8 mode; + + lp8788_read_byte(pchg->lp, LP8788_CHG_EOC, &val); + + mode = val & LP8788_CHG_EOC_MODE_M; + val = (val & LP8788_CHG_EOC_LEVEL_M) >> LP8788_CHG_EOC_LEVEL_S; + level = mode ? abs_level[val] : relative_level[val]; + + return scnprintf(buf, PAGE_SIZE, "End Of Charge Level: %s\n", level); +} + +static DEVICE_ATTR(charger_status, S_IRUSR, lp8788_show_charger_status, NULL); +static DEVICE_ATTR(eoc_time, S_IRUSR, lp8788_show_eoc_time, NULL); +static DEVICE_ATTR(eoc_level, S_IRUSR, lp8788_show_eoc_level, NULL); + +static struct attribute *lp8788_charger_sysfs_attrs[] = { + &dev_attr_charger_status.attr, + &dev_attr_eoc_time.attr, + &dev_attr_eoc_level.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(lp8788_charger_sysfs); + +static int lp8788_psy_register(struct platform_device *pdev, + struct lp8788_charger *pchg) +{ + struct power_supply_config charger_cfg = {}; + + charger_cfg.attr_grp = lp8788_charger_sysfs_groups; + charger_cfg.supplied_to = battery_supplied_to; + charger_cfg.num_supplicants = ARRAY_SIZE(battery_supplied_to); + + pchg->charger = power_supply_register(&pdev->dev, + &lp8788_psy_charger_desc, + &charger_cfg); + if (IS_ERR(pchg->charger)) + return -EPERM; + + pchg->battery = power_supply_register(&pdev->dev, + &lp8788_psy_battery_desc, NULL); + if (IS_ERR(pchg->battery)) { + power_supply_unregister(pchg->charger); + return -EPERM; + } + + return 0; +} + +static int lp8788_charger_probe(struct platform_device *pdev) +{ + struct lp8788 *lp = dev_get_drvdata(pdev->dev.parent); + struct lp8788_charger *pchg; + struct device *dev = &pdev->dev; + int ret; + + pchg = devm_kzalloc(dev, sizeof(struct lp8788_charger), GFP_KERNEL); + if (!pchg) + return -ENOMEM; + + pchg->lp = lp; + pchg->pdata = lp->pdata ? lp->pdata->chg_pdata : NULL; + platform_set_drvdata(pdev, pchg); + + ret = lp8788_update_charger_params(pdev, pchg); + if (ret) + return ret; + + lp8788_setup_adc_channel(&pdev->dev, pchg); + + ret = lp8788_psy_register(pdev, pchg); + if (ret) + return ret; + + ret = lp8788_irq_register(pdev, pchg); + if (ret) + dev_warn(dev, "failed to register charger irq: %d\n", ret); + + return 0; +} + +static int lp8788_charger_remove(struct platform_device *pdev) +{ + struct lp8788_charger *pchg = platform_get_drvdata(pdev); + + flush_work(&pchg->charger_work); + lp8788_irq_unregister(pdev, pchg); + lp8788_psy_unregister(pchg); + + return 0; +} + +static struct platform_driver lp8788_charger_driver = { + .probe = lp8788_charger_probe, + .remove = lp8788_charger_remove, + .driver = { + .name = LP8788_DEV_CHARGER, + }, +}; +module_platform_driver(lp8788_charger_driver); + +MODULE_DESCRIPTION("TI LP8788 Charger Driver"); +MODULE_AUTHOR("Milo Kim"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:lp8788-charger"); diff --git a/drivers/power/supply/lt3651-charger.c b/drivers/power/supply/lt3651-charger.c new file mode 100644 index 000000000..8de500ffa --- /dev/null +++ b/drivers/power/supply/lt3651-charger.c @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for Analog Devices (Linear Technology) LT3651 charger IC. + * Copyright (C) 2017, Topic Embedded Products + */ + +#include <linux/device.h> +#include <linux/gpio/consumer.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/of.h> + +struct lt3651_charger { + struct power_supply *charger; + struct power_supply_desc charger_desc; + struct gpio_desc *acpr_gpio; + struct gpio_desc *fault_gpio; + struct gpio_desc *chrg_gpio; +}; + +static irqreturn_t lt3651_charger_irq(int irq, void *devid) +{ + struct power_supply *charger = devid; + + power_supply_changed(charger); + + return IRQ_HANDLED; +} + +static inline struct lt3651_charger *psy_to_lt3651_charger( + struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static int lt3651_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, union power_supply_propval *val) +{ + struct lt3651_charger *lt3651_charger = psy_to_lt3651_charger(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (!lt3651_charger->chrg_gpio) { + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + if (gpiod_get_value(lt3651_charger->chrg_gpio)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = gpiod_get_value(lt3651_charger->acpr_gpio); + break; + case POWER_SUPPLY_PROP_HEALTH: + if (!lt3651_charger->fault_gpio) { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + break; + } + if (!gpiod_get_value(lt3651_charger->fault_gpio)) { + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + } + /* + * If the fault pin is active, the chrg pin explains the type + * of failure. + */ + if (!lt3651_charger->chrg_gpio) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + } + val->intval = gpiod_get_value(lt3651_charger->chrg_gpio) ? + POWER_SUPPLY_HEALTH_OVERHEAT : + POWER_SUPPLY_HEALTH_DEAD; + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property lt3651_charger_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, +}; + +static int lt3651_charger_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + struct lt3651_charger *lt3651_charger; + struct power_supply_desc *charger_desc; + int ret; + + lt3651_charger = devm_kzalloc(&pdev->dev, sizeof(*lt3651_charger), + GFP_KERNEL); + if (!lt3651_charger) + return -ENOMEM; + + lt3651_charger->acpr_gpio = devm_gpiod_get(&pdev->dev, + "lltc,acpr", GPIOD_IN); + if (IS_ERR(lt3651_charger->acpr_gpio)) { + ret = PTR_ERR(lt3651_charger->acpr_gpio); + dev_err(&pdev->dev, "Failed to acquire acpr GPIO: %d\n", ret); + return ret; + } + lt3651_charger->fault_gpio = devm_gpiod_get_optional(&pdev->dev, + "lltc,fault", GPIOD_IN); + if (IS_ERR(lt3651_charger->fault_gpio)) { + ret = PTR_ERR(lt3651_charger->fault_gpio); + dev_err(&pdev->dev, "Failed to acquire fault GPIO: %d\n", ret); + return ret; + } + lt3651_charger->chrg_gpio = devm_gpiod_get_optional(&pdev->dev, + "lltc,chrg", GPIOD_IN); + if (IS_ERR(lt3651_charger->chrg_gpio)) { + ret = PTR_ERR(lt3651_charger->chrg_gpio); + dev_err(&pdev->dev, "Failed to acquire chrg GPIO: %d\n", ret); + return ret; + } + + charger_desc = <3651_charger->charger_desc; + charger_desc->name = pdev->dev.of_node->name; + charger_desc->type = POWER_SUPPLY_TYPE_MAINS; + charger_desc->properties = lt3651_charger_properties; + charger_desc->num_properties = ARRAY_SIZE(lt3651_charger_properties); + charger_desc->get_property = lt3651_charger_get_property; + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = lt3651_charger; + + lt3651_charger->charger = devm_power_supply_register(&pdev->dev, + charger_desc, &psy_cfg); + if (IS_ERR(lt3651_charger->charger)) { + ret = PTR_ERR(lt3651_charger->charger); + dev_err(&pdev->dev, "Failed to register power supply: %d\n", + ret); + return ret; + } + + /* + * Acquire IRQs for the GPIO pins if possible. If the system does not + * support IRQs on these pins, userspace will have to poll the sysfs + * files manually. + */ + if (lt3651_charger->acpr_gpio) { + ret = gpiod_to_irq(lt3651_charger->acpr_gpio); + if (ret >= 0) + ret = devm_request_any_context_irq(&pdev->dev, ret, + lt3651_charger_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + dev_name(&pdev->dev), lt3651_charger->charger); + if (ret < 0) + dev_warn(&pdev->dev, "Failed to request acpr irq\n"); + } + if (lt3651_charger->fault_gpio) { + ret = gpiod_to_irq(lt3651_charger->fault_gpio); + if (ret >= 0) + ret = devm_request_any_context_irq(&pdev->dev, ret, + lt3651_charger_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + dev_name(&pdev->dev), lt3651_charger->charger); + if (ret < 0) + dev_warn(&pdev->dev, "Failed to request fault irq\n"); + } + if (lt3651_charger->chrg_gpio) { + ret = gpiod_to_irq(lt3651_charger->chrg_gpio); + if (ret >= 0) + ret = devm_request_any_context_irq(&pdev->dev, ret, + lt3651_charger_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + dev_name(&pdev->dev), lt3651_charger->charger); + if (ret < 0) + dev_warn(&pdev->dev, "Failed to request chrg irq\n"); + } + + platform_set_drvdata(pdev, lt3651_charger); + + return 0; +} + +static const struct of_device_id lt3651_charger_match[] = { + { .compatible = "lltc,ltc3651-charger" }, /* DEPRECATED */ + { .compatible = "lltc,lt3651-charger" }, + { } +}; +MODULE_DEVICE_TABLE(of, lt3651_charger_match); + +static struct platform_driver lt3651_charger_driver = { + .probe = lt3651_charger_probe, + .driver = { + .name = "lt3651-charger", + .of_match_table = lt3651_charger_match, + }, +}; + +module_platform_driver(lt3651_charger_driver); + +MODULE_AUTHOR("Mike Looijmans <mike.looijmans@topic.nl>"); +MODULE_DESCRIPTION("Driver for LT3651 charger"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:lt3651-charger"); diff --git a/drivers/power/supply/ltc2941-battery-gauge.c b/drivers/power/supply/ltc2941-battery-gauge.c new file mode 100644 index 000000000..10cd61751 --- /dev/null +++ b/drivers/power/supply/ltc2941-battery-gauge.c @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * I2C client/driver for the Linear Technology LTC2941, LTC2942, LTC2943 + * and LTC2944 Battery Gas Gauge IC + * + * Copyright (C) 2014 Topic Embedded Systems + * + * Author: Auryn Verwegen + * Author: Mike Looijmans + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/types.h> +#include <linux/errno.h> +#include <linux/swab.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#define I16_MSB(x) ((x >> 8) & 0xFF) +#define I16_LSB(x) (x & 0xFF) + +#define LTC294X_WORK_DELAY 10 /* Update delay in seconds */ + +#define LTC294X_MAX_VALUE 0xFFFF +#define LTC294X_MID_SUPPLY 0x7FFF + +#define LTC2941_MAX_PRESCALER_EXP 7 +#define LTC2943_MAX_PRESCALER_EXP 6 + +enum ltc294x_reg { + LTC294X_REG_STATUS = 0x00, + LTC294X_REG_CONTROL = 0x01, + LTC294X_REG_ACC_CHARGE_MSB = 0x02, + LTC294X_REG_ACC_CHARGE_LSB = 0x03, + LTC294X_REG_CHARGE_THR_HIGH_MSB = 0x04, + LTC294X_REG_CHARGE_THR_HIGH_LSB = 0x05, + LTC294X_REG_CHARGE_THR_LOW_MSB = 0x06, + LTC294X_REG_CHARGE_THR_LOW_LSB = 0x07, + LTC294X_REG_VOLTAGE_MSB = 0x08, + LTC294X_REG_VOLTAGE_LSB = 0x09, + LTC2942_REG_TEMPERATURE_MSB = 0x0C, + LTC2942_REG_TEMPERATURE_LSB = 0x0D, + LTC2943_REG_CURRENT_MSB = 0x0E, + LTC2943_REG_CURRENT_LSB = 0x0F, + LTC2943_REG_TEMPERATURE_MSB = 0x14, + LTC2943_REG_TEMPERATURE_LSB = 0x15, +}; + +enum ltc294x_id { + LTC2941_ID, + LTC2942_ID, + LTC2943_ID, + LTC2944_ID, +}; + +#define LTC2941_REG_STATUS_CHIP_ID BIT(7) + +#define LTC2942_REG_CONTROL_MODE_SCAN (BIT(7) | BIT(6)) +#define LTC2943_REG_CONTROL_MODE_SCAN BIT(7) +#define LTC294X_REG_CONTROL_PRESCALER_MASK (BIT(5) | BIT(4) | BIT(3)) +#define LTC294X_REG_CONTROL_SHUTDOWN_MASK (BIT(0)) +#define LTC294X_REG_CONTROL_PRESCALER_SET(x) \ + ((x << 3) & LTC294X_REG_CONTROL_PRESCALER_MASK) +#define LTC294X_REG_CONTROL_ALCC_CONFIG_DISABLED 0 +#define LTC294X_REG_CONTROL_ADC_DISABLE(x) ((x) & ~(BIT(7) | BIT(6))) + +struct ltc294x_info { + struct i2c_client *client; /* I2C Client pointer */ + struct power_supply *supply; /* Supply pointer */ + struct power_supply_desc supply_desc; /* Supply description */ + struct delayed_work work; /* Work scheduler */ + enum ltc294x_id id; /* Chip type */ + int charge; /* Last charge register content */ + int r_sense; /* mOhm */ + int Qlsb; /* nAh */ +}; + +static inline int convert_bin_to_uAh( + const struct ltc294x_info *info, int Q) +{ + return ((Q * (info->Qlsb / 10))) / 100; +} + +static inline int convert_uAh_to_bin( + const struct ltc294x_info *info, int uAh) +{ + int Q; + + Q = (uAh * 100) / (info->Qlsb/10); + return (Q < LTC294X_MAX_VALUE) ? Q : LTC294X_MAX_VALUE; +} + +static int ltc294x_read_regs(struct i2c_client *client, + enum ltc294x_reg reg, u8 *buf, int num_regs) +{ + int ret; + struct i2c_msg msgs[2] = { }; + u8 reg_start = reg; + + msgs[0].addr = client->addr; + msgs[0].len = 1; + msgs[0].buf = ®_start; + + msgs[1].addr = client->addr; + msgs[1].len = num_regs; + msgs[1].buf = buf; + msgs[1].flags = I2C_M_RD; + + ret = i2c_transfer(client->adapter, &msgs[0], 2); + if (ret < 0) { + dev_err(&client->dev, "ltc2941 read_reg failed!\n"); + return ret; + } + + dev_dbg(&client->dev, "%s (%#x, %d) -> %#x\n", + __func__, reg, num_regs, *buf); + + return 0; +} + +static int ltc294x_write_regs(struct i2c_client *client, + enum ltc294x_reg reg, const u8 *buf, int num_regs) +{ + int ret; + u8 reg_start = reg; + + ret = i2c_smbus_write_i2c_block_data(client, reg_start, num_regs, buf); + if (ret < 0) { + dev_err(&client->dev, "ltc2941 write_reg failed!\n"); + return ret; + } + + dev_dbg(&client->dev, "%s (%#x, %d) -> %#x\n", + __func__, reg, num_regs, *buf); + + return 0; +} + +static int ltc294x_reset(const struct ltc294x_info *info, int prescaler_exp) +{ + int ret; + u8 value; + u8 control; + + /* Read status and control registers */ + ret = ltc294x_read_regs(info->client, LTC294X_REG_CONTROL, &value, 1); + if (ret < 0) { + dev_err(&info->client->dev, + "Could not read registers from device\n"); + goto error_exit; + } + + control = LTC294X_REG_CONTROL_PRESCALER_SET(prescaler_exp) | + LTC294X_REG_CONTROL_ALCC_CONFIG_DISABLED; + /* Put device into "monitor" mode */ + switch (info->id) { + case LTC2942_ID: /* 2942 measures every 2 sec */ + control |= LTC2942_REG_CONTROL_MODE_SCAN; + break; + case LTC2943_ID: + case LTC2944_ID: /* 2943 and 2944 measure every 10 sec */ + control |= LTC2943_REG_CONTROL_MODE_SCAN; + break; + default: + break; + } + + if (value != control) { + ret = ltc294x_write_regs(info->client, + LTC294X_REG_CONTROL, &control, 1); + if (ret < 0) { + dev_err(&info->client->dev, + "Could not write register\n"); + goto error_exit; + } + } + + return 0; + +error_exit: + return ret; +} + +static int ltc294x_read_charge_register(const struct ltc294x_info *info, + enum ltc294x_reg reg) + { + int ret; + u8 datar[2]; + + ret = ltc294x_read_regs(info->client, reg, &datar[0], 2); + if (ret < 0) + return ret; + return (datar[0] << 8) + datar[1]; +} + +static int ltc294x_get_charge(const struct ltc294x_info *info, + enum ltc294x_reg reg, int *val) +{ + int value = ltc294x_read_charge_register(info, reg); + + if (value < 0) + return value; + /* When r_sense < 0, this counts up when the battery discharges */ + if (info->Qlsb < 0) + value -= 0xFFFF; + *val = convert_bin_to_uAh(info, value); + return 0; +} + +static int ltc294x_set_charge_now(const struct ltc294x_info *info, int val) +{ + int ret; + u8 dataw[2]; + u8 ctrl_reg; + s32 value; + + value = convert_uAh_to_bin(info, val); + /* Direction depends on how sense+/- were connected */ + if (info->Qlsb < 0) + value += 0xFFFF; + if ((value < 0) || (value > 0xFFFF)) /* input validation */ + return -EINVAL; + + /* Read control register */ + ret = ltc294x_read_regs(info->client, + LTC294X_REG_CONTROL, &ctrl_reg, 1); + if (ret < 0) + return ret; + /* Disable analog section */ + ctrl_reg |= LTC294X_REG_CONTROL_SHUTDOWN_MASK; + ret = ltc294x_write_regs(info->client, + LTC294X_REG_CONTROL, &ctrl_reg, 1); + if (ret < 0) + return ret; + /* Set new charge value */ + dataw[0] = I16_MSB(value); + dataw[1] = I16_LSB(value); + ret = ltc294x_write_regs(info->client, + LTC294X_REG_ACC_CHARGE_MSB, &dataw[0], 2); + if (ret < 0) + goto error_exit; + /* Enable analog section */ +error_exit: + ctrl_reg &= ~LTC294X_REG_CONTROL_SHUTDOWN_MASK; + ret = ltc294x_write_regs(info->client, + LTC294X_REG_CONTROL, &ctrl_reg, 1); + + return ret < 0 ? ret : 0; +} + +static int ltc294x_set_charge_thr(const struct ltc294x_info *info, + enum ltc294x_reg reg, int val) +{ + u8 dataw[2]; + s32 value; + + value = convert_uAh_to_bin(info, val); + /* Direction depends on how sense+/- were connected */ + if (info->Qlsb < 0) + value += 0xFFFF; + if ((value < 0) || (value > 0xFFFF)) /* input validation */ + return -EINVAL; + + /* Set new charge value */ + dataw[0] = I16_MSB(value); + dataw[1] = I16_LSB(value); + return ltc294x_write_regs(info->client, reg, &dataw[0], 2); +} + +static int ltc294x_get_charge_counter( + const struct ltc294x_info *info, int *val) +{ + int value = ltc294x_read_charge_register(info, LTC294X_REG_ACC_CHARGE_MSB); + + if (value < 0) + return value; + value -= LTC294X_MID_SUPPLY; + *val = convert_bin_to_uAh(info, value); + return 0; +} + +static int ltc294x_get_voltage(const struct ltc294x_info *info, int *val) +{ + int ret; + u8 datar[2]; + u32 value; + + ret = ltc294x_read_regs(info->client, + LTC294X_REG_VOLTAGE_MSB, &datar[0], 2); + value = (datar[0] << 8) | datar[1]; + switch (info->id) { + case LTC2943_ID: + value *= 23600 * 2; + value /= 0xFFFF; + value *= 1000 / 2; + break; + case LTC2944_ID: + value *= 70800 / 5*4; + value /= 0xFFFF; + value *= 1000 * 5/4; + break; + default: + value *= 6000 * 10; + value /= 0xFFFF; + value *= 1000 / 10; + break; + } + *val = value; + return ret; +} + +static int ltc294x_get_current(const struct ltc294x_info *info, int *val) +{ + int ret; + u8 datar[2]; + s32 value; + + ret = ltc294x_read_regs(info->client, + LTC2943_REG_CURRENT_MSB, &datar[0], 2); + value = (datar[0] << 8) | datar[1]; + value -= 0x7FFF; + if (info->id == LTC2944_ID) + value *= 64000; + else + value *= 60000; + /* Value is in range -32k..+32k, r_sense is usually 10..50 mOhm, + * the formula below keeps everything in s32 range while preserving + * enough digits */ + *val = 1000 * (value / (info->r_sense * 0x7FFF)); /* in uA */ + return ret; +} + +static int ltc294x_get_temperature(const struct ltc294x_info *info, int *val) +{ + enum ltc294x_reg reg; + int ret; + u8 datar[2]; + u32 value; + + if (info->id == LTC2942_ID) { + reg = LTC2942_REG_TEMPERATURE_MSB; + value = 6000; /* Full-scale is 600 Kelvin */ + } else { + reg = LTC2943_REG_TEMPERATURE_MSB; + value = 5100; /* Full-scale is 510 Kelvin */ + } + ret = ltc294x_read_regs(info->client, reg, &datar[0], 2); + value *= (datar[0] << 8) | datar[1]; + /* Convert to tenths of degree Celsius */ + *val = value / 0xFFFF - 2722; + return ret; +} + +static int ltc294x_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct ltc294x_info *info = power_supply_get_drvdata(psy); + + switch (prop) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + return ltc294x_get_charge(info, LTC294X_REG_CHARGE_THR_HIGH_MSB, + &val->intval); + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + return ltc294x_get_charge(info, LTC294X_REG_CHARGE_THR_LOW_MSB, + &val->intval); + case POWER_SUPPLY_PROP_CHARGE_NOW: + return ltc294x_get_charge(info, LTC294X_REG_ACC_CHARGE_MSB, + &val->intval); + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + return ltc294x_get_charge_counter(info, &val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return ltc294x_get_voltage(info, &val->intval); + case POWER_SUPPLY_PROP_CURRENT_NOW: + return ltc294x_get_current(info, &val->intval); + case POWER_SUPPLY_PROP_TEMP: + return ltc294x_get_temperature(info, &val->intval); + default: + return -EINVAL; + } +} + +static int ltc294x_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct ltc294x_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + return ltc294x_set_charge_thr(info, + LTC294X_REG_CHARGE_THR_HIGH_MSB, val->intval); + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + return ltc294x_set_charge_thr(info, + LTC294X_REG_CHARGE_THR_LOW_MSB, val->intval); + case POWER_SUPPLY_PROP_CHARGE_NOW: + return ltc294x_set_charge_now(info, val->intval); + default: + return -EPERM; + } +} + +static int ltc294x_property_is_writeable( + struct power_supply *psy, enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + case POWER_SUPPLY_PROP_CHARGE_NOW: + return 1; + default: + return 0; + } +} + +static void ltc294x_update(struct ltc294x_info *info) +{ + int charge = ltc294x_read_charge_register(info, LTC294X_REG_ACC_CHARGE_MSB); + + if (charge != info->charge) { + info->charge = charge; + power_supply_changed(info->supply); + } +} + +static void ltc294x_work(struct work_struct *work) +{ + struct ltc294x_info *info; + + info = container_of(work, struct ltc294x_info, work.work); + ltc294x_update(info); + schedule_delayed_work(&info->work, LTC294X_WORK_DELAY * HZ); +} + +static enum power_supply_property ltc294x_properties[] = { + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_EMPTY, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static int ltc294x_i2c_remove(struct i2c_client *client) +{ + struct ltc294x_info *info = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&info->work); + power_supply_unregister(info->supply); + return 0; +} + +static int ltc294x_i2c_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct power_supply_config psy_cfg = {}; + struct ltc294x_info *info; + struct device_node *np; + int ret; + u32 prescaler_exp; + s32 r_sense; + u8 status; + + info = devm_kzalloc(&client->dev, sizeof(*info), GFP_KERNEL); + if (info == NULL) + return -ENOMEM; + + i2c_set_clientdata(client, info); + + np = of_node_get(client->dev.of_node); + + info->id = (enum ltc294x_id) (uintptr_t) of_device_get_match_data( + &client->dev); + info->supply_desc.name = np->name; + + /* r_sense can be negative, when sense+ is connected to the battery + * instead of the sense-. This results in reversed measurements. */ + ret = of_property_read_u32(np, "lltc,resistor-sense", &r_sense); + if (ret < 0) { + dev_err(&client->dev, + "Could not find lltc,resistor-sense in devicetree\n"); + return ret; + } + info->r_sense = r_sense; + + ret = of_property_read_u32(np, "lltc,prescaler-exponent", + &prescaler_exp); + if (ret < 0) { + dev_warn(&client->dev, + "lltc,prescaler-exponent not in devicetree\n"); + prescaler_exp = LTC2941_MAX_PRESCALER_EXP; + } + + if (info->id == LTC2943_ID) { + if (prescaler_exp > LTC2943_MAX_PRESCALER_EXP) + prescaler_exp = LTC2943_MAX_PRESCALER_EXP; + info->Qlsb = ((340 * 50000) / r_sense) / + (4096 / (1 << (2*prescaler_exp))); + } else { + if (prescaler_exp > LTC2941_MAX_PRESCALER_EXP) + prescaler_exp = LTC2941_MAX_PRESCALER_EXP; + info->Qlsb = ((85 * 50000) / r_sense) / + (128 / (1 << prescaler_exp)); + } + + /* Read status register to check for LTC2942 */ + if (info->id == LTC2941_ID || info->id == LTC2942_ID) { + ret = ltc294x_read_regs(client, LTC294X_REG_STATUS, &status, 1); + if (ret < 0) { + dev_err(&client->dev, + "Could not read status register\n"); + return ret; + } + if (status & LTC2941_REG_STATUS_CHIP_ID) + info->id = LTC2941_ID; + else + info->id = LTC2942_ID; + } + + info->client = client; + info->supply_desc.type = POWER_SUPPLY_TYPE_BATTERY; + info->supply_desc.properties = ltc294x_properties; + switch (info->id) { + case LTC2944_ID: + case LTC2943_ID: + info->supply_desc.num_properties = + ARRAY_SIZE(ltc294x_properties); + break; + case LTC2942_ID: + info->supply_desc.num_properties = + ARRAY_SIZE(ltc294x_properties) - 1; + break; + case LTC2941_ID: + default: + info->supply_desc.num_properties = + ARRAY_SIZE(ltc294x_properties) - 3; + break; + } + info->supply_desc.get_property = ltc294x_get_property; + info->supply_desc.set_property = ltc294x_set_property; + info->supply_desc.property_is_writeable = ltc294x_property_is_writeable; + info->supply_desc.external_power_changed = NULL; + + psy_cfg.drv_data = info; + + INIT_DELAYED_WORK(&info->work, ltc294x_work); + + ret = ltc294x_reset(info, prescaler_exp); + if (ret < 0) { + dev_err(&client->dev, "Communication with chip failed\n"); + return ret; + } + + info->supply = power_supply_register(&client->dev, &info->supply_desc, + &psy_cfg); + if (IS_ERR(info->supply)) { + dev_err(&client->dev, "failed to register ltc2941\n"); + return PTR_ERR(info->supply); + } else { + schedule_delayed_work(&info->work, LTC294X_WORK_DELAY * HZ); + } + + return 0; +} + +static void ltc294x_i2c_shutdown(struct i2c_client *client) +{ + struct ltc294x_info *info = i2c_get_clientdata(client); + int ret; + u8 value; + u8 control; + + /* The LTC2941 does not need any special handling */ + if (info->id == LTC2941_ID) + return; + + /* Read control register */ + ret = ltc294x_read_regs(info->client, LTC294X_REG_CONTROL, &value, 1); + if (ret < 0) + return; + + /* Disable continuous ADC conversion as this drains the battery */ + control = LTC294X_REG_CONTROL_ADC_DISABLE(value); + if (control != value) + ltc294x_write_regs(info->client, LTC294X_REG_CONTROL, + &control, 1); +} + +#ifdef CONFIG_PM_SLEEP + +static int ltc294x_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct ltc294x_info *info = i2c_get_clientdata(client); + + cancel_delayed_work(&info->work); + return 0; +} + +static int ltc294x_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct ltc294x_info *info = i2c_get_clientdata(client); + + schedule_delayed_work(&info->work, LTC294X_WORK_DELAY * HZ); + return 0; +} + +static SIMPLE_DEV_PM_OPS(ltc294x_pm_ops, ltc294x_suspend, ltc294x_resume); +#define LTC294X_PM_OPS (<c294x_pm_ops) + +#else +#define LTC294X_PM_OPS NULL +#endif /* CONFIG_PM_SLEEP */ + + +static const struct i2c_device_id ltc294x_i2c_id[] = { + { "ltc2941", LTC2941_ID, }, + { "ltc2942", LTC2942_ID, }, + { "ltc2943", LTC2943_ID, }, + { "ltc2944", LTC2944_ID, }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, ltc294x_i2c_id); + +static const struct of_device_id ltc294x_i2c_of_match[] = { + { + .compatible = "lltc,ltc2941", + .data = (void *)LTC2941_ID, + }, + { + .compatible = "lltc,ltc2942", + .data = (void *)LTC2942_ID, + }, + { + .compatible = "lltc,ltc2943", + .data = (void *)LTC2943_ID, + }, + { + .compatible = "lltc,ltc2944", + .data = (void *)LTC2944_ID, + }, + { }, +}; +MODULE_DEVICE_TABLE(of, ltc294x_i2c_of_match); + +static struct i2c_driver ltc294x_driver = { + .driver = { + .name = "LTC2941", + .of_match_table = ltc294x_i2c_of_match, + .pm = LTC294X_PM_OPS, + }, + .probe = ltc294x_i2c_probe, + .remove = ltc294x_i2c_remove, + .shutdown = ltc294x_i2c_shutdown, + .id_table = ltc294x_i2c_id, +}; +module_i2c_driver(ltc294x_driver); + +MODULE_AUTHOR("Auryn Verwegen, Topic Embedded Systems"); +MODULE_AUTHOR("Mike Looijmans, Topic Embedded Products"); +MODULE_DESCRIPTION("LTC2941/LTC2942/LTC2943/LTC2944 Battery Gas Gauge IC driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max14577_charger.c b/drivers/power/supply/max14577_charger.c new file mode 100644 index 000000000..dcedae18d --- /dev/null +++ b/drivers/power/supply/max14577_charger.c @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// max14577_charger.c - Battery charger driver for the Maxim 14577/77836 +// +// Copyright (C) 2013,2014 Samsung Electronics +// Krzysztof Kozlowski <krzk@kernel.org> + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/max14577-private.h> +#include <linux/mfd/max14577.h> + +struct max14577_charger { + struct device *dev; + struct max14577 *max14577; + struct power_supply *charger; + + struct max14577_charger_platform_data *pdata; +}; + +/* + * Helper function for mapping values of STATUS2/CHGTYP register on max14577 + * and max77836 chipsets to enum maxim_muic_charger_type. + */ +static enum max14577_muic_charger_type maxim_get_charger_type( + enum maxim_device_type dev_type, u8 val) { + switch (val) { + case MAX14577_CHARGER_TYPE_NONE: + case MAX14577_CHARGER_TYPE_USB: + case MAX14577_CHARGER_TYPE_DOWNSTREAM_PORT: + case MAX14577_CHARGER_TYPE_DEDICATED_CHG: + case MAX14577_CHARGER_TYPE_SPECIAL_500MA: + case MAX14577_CHARGER_TYPE_SPECIAL_1A: + return val; + case MAX14577_CHARGER_TYPE_DEAD_BATTERY: + case MAX14577_CHARGER_TYPE_RESERVED: + if (dev_type == MAXIM_DEVICE_TYPE_MAX77836) + val |= 0x8; + return val; + default: + WARN_ONCE(1, "max14577: Unsupported chgtyp register value 0x%02x", val); + return val; + } +} + +static int max14577_get_charger_state(struct max14577_charger *chg, int *val) +{ + struct regmap *rmap = chg->max14577->regmap; + int ret; + u8 reg_data; + + /* + * Charging occurs only if: + * - CHGCTRL2/MBCHOSTEN == 1 + * - STATUS2/CGMBC == 1 + * + * TODO: + * - handle FULL after Top-off timer (EOC register may be off + * and the charger won't be charging although MBCHOSTEN is on) + * - handle properly dead-battery charging (respect timer) + * - handle timers (fast-charge and prequal) /MBCCHGERR/ + */ + ret = max14577_read_reg(rmap, MAX14577_CHG_REG_CHG_CTRL2, ®_data); + if (ret < 0) + goto out; + + if ((reg_data & CHGCTRL2_MBCHOSTEN_MASK) == 0) { + *val = POWER_SUPPLY_STATUS_DISCHARGING; + goto out; + } + + ret = max14577_read_reg(rmap, MAX14577_CHG_REG_STATUS3, ®_data); + if (ret < 0) + goto out; + + if (reg_data & STATUS3_CGMBC_MASK) { + /* Charger or USB-cable is connected */ + if (reg_data & STATUS3_EOC_MASK) + *val = POWER_SUPPLY_STATUS_FULL; + else + *val = POWER_SUPPLY_STATUS_CHARGING; + goto out; + } + + *val = POWER_SUPPLY_STATUS_DISCHARGING; + +out: + return ret; +} + +/* + * Supported charge types: + * - POWER_SUPPLY_CHARGE_TYPE_NONE + * - POWER_SUPPLY_CHARGE_TYPE_FAST + */ +static int max14577_get_charge_type(struct max14577_charger *chg, int *val) +{ + int ret, charging; + + /* + * TODO: CHARGE_TYPE_TRICKLE (VCHGR_RC or EOC)? + * As spec says: + * [after reaching EOC interrupt] + * "When the battery is fully charged, the 30-minute (typ) + * top-off timer starts. The device continues to trickle + * charge the battery until the top-off timer runs out." + */ + ret = max14577_get_charger_state(chg, &charging); + if (ret < 0) + return ret; + + if (charging == POWER_SUPPLY_STATUS_CHARGING) + *val = POWER_SUPPLY_CHARGE_TYPE_FAST; + else + *val = POWER_SUPPLY_CHARGE_TYPE_NONE; + + return 0; +} + +static int max14577_get_online(struct max14577_charger *chg, int *val) +{ + struct regmap *rmap = chg->max14577->regmap; + u8 reg_data; + int ret; + enum max14577_muic_charger_type chg_type; + + ret = max14577_read_reg(rmap, MAX14577_MUIC_REG_STATUS2, ®_data); + if (ret < 0) + return ret; + + reg_data = ((reg_data & STATUS2_CHGTYP_MASK) >> STATUS2_CHGTYP_SHIFT); + chg_type = maxim_get_charger_type(chg->max14577->dev_type, reg_data); + switch (chg_type) { + case MAX14577_CHARGER_TYPE_USB: + case MAX14577_CHARGER_TYPE_DEDICATED_CHG: + case MAX14577_CHARGER_TYPE_SPECIAL_500MA: + case MAX14577_CHARGER_TYPE_SPECIAL_1A: + case MAX14577_CHARGER_TYPE_DEAD_BATTERY: + case MAX77836_CHARGER_TYPE_SPECIAL_BIAS: + *val = 1; + break; + case MAX14577_CHARGER_TYPE_NONE: + case MAX14577_CHARGER_TYPE_DOWNSTREAM_PORT: + case MAX14577_CHARGER_TYPE_RESERVED: + case MAX77836_CHARGER_TYPE_RESERVED: + default: + *val = 0; + } + + return 0; +} + +/* + * Supported health statuses: + * - POWER_SUPPLY_HEALTH_DEAD + * - POWER_SUPPLY_HEALTH_OVERVOLTAGE + * - POWER_SUPPLY_HEALTH_GOOD + */ +static int max14577_get_battery_health(struct max14577_charger *chg, int *val) +{ + struct regmap *rmap = chg->max14577->regmap; + int ret; + u8 reg_data; + enum max14577_muic_charger_type chg_type; + + ret = max14577_read_reg(rmap, MAX14577_MUIC_REG_STATUS2, ®_data); + if (ret < 0) + goto out; + + reg_data = ((reg_data & STATUS2_CHGTYP_MASK) >> STATUS2_CHGTYP_SHIFT); + chg_type = maxim_get_charger_type(chg->max14577->dev_type, reg_data); + if (chg_type == MAX14577_CHARGER_TYPE_DEAD_BATTERY) { + *val = POWER_SUPPLY_HEALTH_DEAD; + goto out; + } + + ret = max14577_read_reg(rmap, MAX14577_CHG_REG_STATUS3, ®_data); + if (ret < 0) + goto out; + + if (reg_data & STATUS3_OVP_MASK) { + *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + goto out; + } + + /* Not dead, not overvoltage */ + *val = POWER_SUPPLY_HEALTH_GOOD; + +out: + return ret; +} + +/* + * Always returns 1. + * The max14577 chip doesn't report any status of battery presence. + * Lets assume that it will always be used with some battery. + */ +static int max14577_get_present(struct max14577_charger *chg, int *val) +{ + *val = 1; + + return 0; +} + +static int max14577_set_fast_charge_timer(struct max14577_charger *chg, + unsigned long hours) +{ + u8 reg_data; + + switch (hours) { + case 5 ... 7: + reg_data = hours - 3; + break; + case 0: + /* Disable */ + reg_data = 0x7; + break; + default: + dev_err(chg->dev, "Wrong value for Fast-Charge Timer: %lu\n", + hours); + return -EINVAL; + } + reg_data <<= CHGCTRL1_TCHW_SHIFT; + + return max14577_update_reg(chg->max14577->regmap, + MAX14577_REG_CHGCTRL1, CHGCTRL1_TCHW_MASK, reg_data); +} + +static int max14577_init_constant_voltage(struct max14577_charger *chg, + unsigned int uvolt) +{ + u8 reg_data; + + if (uvolt < MAXIM_CHARGER_CONSTANT_VOLTAGE_MIN || + uvolt > MAXIM_CHARGER_CONSTANT_VOLTAGE_MAX) + return -EINVAL; + + if (uvolt == 4200000) + reg_data = 0x0; + else if (uvolt == MAXIM_CHARGER_CONSTANT_VOLTAGE_MAX) + reg_data = 0x1f; + else if (uvolt <= 4280000) { + unsigned int val = uvolt; + + val -= MAXIM_CHARGER_CONSTANT_VOLTAGE_MIN; + val /= MAXIM_CHARGER_CONSTANT_VOLTAGE_STEP; + if (uvolt <= 4180000) + reg_data = 0x1 + val; + else + reg_data = val; /* Fix for gap between 4.18V and 4.22V */ + } else + return -EINVAL; + + reg_data <<= CHGCTRL3_MBCCVWRC_SHIFT; + + return max14577_write_reg(chg->max14577->regmap, + MAX14577_CHG_REG_CHG_CTRL3, reg_data); +} + +static int max14577_init_eoc(struct max14577_charger *chg, + unsigned int uamp) +{ + unsigned int current_bits = 0xf; + u8 reg_data; + + switch (chg->max14577->dev_type) { + case MAXIM_DEVICE_TYPE_MAX77836: + if (uamp < 5000) + return -EINVAL; /* Requested current is too low */ + + if (uamp >= 7500 && uamp < 10000) + current_bits = 0x0; + else if (uamp <= 50000) { + /* <5000, 7499> and <10000, 50000> */ + current_bits = uamp / 5000; + } else { + uamp = min(uamp, 100000U) - 50000U; + current_bits = 0xa + uamp / 10000; + } + break; + + case MAXIM_DEVICE_TYPE_MAX14577: + default: + if (uamp < MAX14577_CHARGER_EOC_CURRENT_LIMIT_MIN) + return -EINVAL; /* Requested current is too low */ + + uamp = min(uamp, MAX14577_CHARGER_EOC_CURRENT_LIMIT_MAX); + uamp -= MAX14577_CHARGER_EOC_CURRENT_LIMIT_MIN; + current_bits = uamp / MAX14577_CHARGER_EOC_CURRENT_LIMIT_STEP; + break; + } + + reg_data = current_bits << CHGCTRL5_EOCS_SHIFT; + + return max14577_update_reg(chg->max14577->regmap, + MAX14577_CHG_REG_CHG_CTRL5, CHGCTRL5_EOCS_MASK, + reg_data); +} + +static int max14577_init_fast_charge(struct max14577_charger *chg, + unsigned int uamp) +{ + u8 reg_data; + int ret; + const struct maxim_charger_current *limits = + &maxim_charger_currents[chg->max14577->dev_type]; + + ret = maxim_charger_calc_reg_current(limits, uamp, uamp, ®_data); + if (ret) { + dev_err(chg->dev, "Wrong value for fast charge: %u\n", uamp); + return ret; + } + + return max14577_update_reg(chg->max14577->regmap, + MAX14577_CHG_REG_CHG_CTRL4, + CHGCTRL4_MBCICHWRCL_MASK | CHGCTRL4_MBCICHWRCH_MASK, + reg_data); +} + +/* + * Sets charger registers to proper and safe default values. + * Some of these values are equal to defaults in MAX14577E + * data sheet but there are minor differences. + */ +static int max14577_charger_reg_init(struct max14577_charger *chg) +{ + struct regmap *rmap = chg->max14577->regmap; + u8 reg_data; + int ret; + + /* + * Charger-Type Manual Detection, default off (set CHGTYPMAN to 0) + * Charger-Detection Enable, default on (set CHGDETEN to 1) + * Combined mask of CHGDETEN and CHGTYPMAN will zero the CHGTYPMAN bit + */ + reg_data = 0x1 << CDETCTRL1_CHGDETEN_SHIFT; + max14577_update_reg(rmap, MAX14577_REG_CDETCTRL1, + CDETCTRL1_CHGDETEN_MASK | CDETCTRL1_CHGTYPMAN_MASK, + reg_data); + + /* + * Wall-Adapter Rapid Charge, default on + * Battery-Charger, default on + */ + reg_data = 0x1 << CHGCTRL2_VCHGR_RC_SHIFT; + reg_data |= 0x1 << CHGCTRL2_MBCHOSTEN_SHIFT; + max14577_write_reg(rmap, MAX14577_REG_CHGCTRL2, reg_data); + + /* Auto Charging Stop, default off */ + reg_data = 0x0 << CHGCTRL6_AUTOSTOP_SHIFT; + max14577_write_reg(rmap, MAX14577_REG_CHGCTRL6, reg_data); + + ret = max14577_init_constant_voltage(chg, chg->pdata->constant_uvolt); + if (ret) + return ret; + + ret = max14577_init_eoc(chg, chg->pdata->eoc_uamp); + if (ret) + return ret; + + ret = max14577_init_fast_charge(chg, chg->pdata->fast_charge_uamp); + if (ret) + return ret; + + ret = max14577_set_fast_charge_timer(chg, + MAXIM_CHARGER_FAST_CHARGE_TIMER_DEFAULT); + if (ret) + return ret; + + /* Initialize Overvoltage-Protection Threshold */ + switch (chg->pdata->ovp_uvolt) { + case 7500000: + reg_data = 0x0; + break; + case 6000000: + case 6500000: + case 7000000: + reg_data = 0x1 + (chg->pdata->ovp_uvolt - 6000000) / 500000; + break; + default: + dev_err(chg->dev, "Wrong value for OVP: %u\n", + chg->pdata->ovp_uvolt); + return -EINVAL; + } + reg_data <<= CHGCTRL7_OTPCGHCVS_SHIFT; + max14577_write_reg(rmap, MAX14577_REG_CHGCTRL7, reg_data); + + return 0; +} + +/* Support property from charger */ +static enum power_supply_property max14577_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static const char * const model_names[] = { + [MAXIM_DEVICE_TYPE_UNKNOWN] = "MAX14577-like", + [MAXIM_DEVICE_TYPE_MAX14577] = "MAX14577", + [MAXIM_DEVICE_TYPE_MAX77836] = "MAX77836", +}; +static const char *manufacturer = "Maxim Integrated"; + +static int max14577_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max14577_charger *chg = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = max14577_get_charger_state(chg, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = max14577_get_charge_type(chg, &val->intval); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = max14577_get_battery_health(chg, &val->intval); + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = max14577_get_present(chg, &val->intval); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = max14577_get_online(chg, &val->intval); + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + BUILD_BUG_ON(ARRAY_SIZE(model_names) != MAXIM_DEVICE_TYPE_NUM); + val->strval = model_names[chg->max14577->dev_type]; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = manufacturer; + break; + default: + return -EINVAL; + } + + return ret; +} + +static const struct power_supply_desc max14577_charger_desc = { + .name = "max14577-charger", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = max14577_charger_props, + .num_properties = ARRAY_SIZE(max14577_charger_props), + .get_property = max14577_charger_get_property, +}; + +#ifdef CONFIG_OF +static struct max14577_charger_platform_data *max14577_charger_dt_init( + struct platform_device *pdev) +{ + struct max14577_charger_platform_data *pdata; + struct device_node *np = pdev->dev.of_node; + int ret; + + if (!np) { + dev_err(&pdev->dev, "No charger OF node\n"); + return ERR_PTR(-EINVAL); + } + + pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + ret = of_property_read_u32(np, "maxim,constant-uvolt", + &pdata->constant_uvolt); + if (ret) { + dev_err(&pdev->dev, "Cannot parse maxim,constant-uvolt field from DT\n"); + return ERR_PTR(ret); + } + + ret = of_property_read_u32(np, "maxim,fast-charge-uamp", + &pdata->fast_charge_uamp); + if (ret) { + dev_err(&pdev->dev, "Cannot parse maxim,fast-charge-uamp field from DT\n"); + return ERR_PTR(ret); + } + + ret = of_property_read_u32(np, "maxim,eoc-uamp", &pdata->eoc_uamp); + if (ret) { + dev_err(&pdev->dev, "Cannot parse maxim,eoc-uamp field from DT\n"); + return ERR_PTR(ret); + } + + ret = of_property_read_u32(np, "maxim,ovp-uvolt", &pdata->ovp_uvolt); + if (ret) { + dev_err(&pdev->dev, "Cannot parse maxim,ovp-uvolt field from DT\n"); + return ERR_PTR(ret); + } + + return pdata; +} +#else /* CONFIG_OF */ +static struct max14577_charger_platform_data *max14577_charger_dt_init( + struct platform_device *pdev) +{ + return NULL; +} +#endif /* CONFIG_OF */ + +static ssize_t show_fast_charge_timer(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct max14577_charger *chg = dev_get_drvdata(dev); + u8 reg_data; + int ret; + unsigned int val; + + ret = max14577_read_reg(chg->max14577->regmap, MAX14577_REG_CHGCTRL1, + ®_data); + if (ret) + return ret; + + reg_data &= CHGCTRL1_TCHW_MASK; + reg_data >>= CHGCTRL1_TCHW_SHIFT; + switch (reg_data) { + case 0x2 ... 0x4: + val = reg_data + 3; + break; + case 0x7: + val = 0; + break; + default: + val = 5; + break; + } + + return scnprintf(buf, PAGE_SIZE, "%u\n", val); +} + +static ssize_t store_fast_charge_timer(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct max14577_charger *chg = dev_get_drvdata(dev); + unsigned long val; + int ret; + + ret = kstrtoul(buf, 10, &val); + if (ret) + return ret; + + ret = max14577_set_fast_charge_timer(chg, val); + if (ret) + return ret; + + return count; +} + +static DEVICE_ATTR(fast_charge_timer, S_IRUGO | S_IWUSR, + show_fast_charge_timer, store_fast_charge_timer); + +static int max14577_charger_probe(struct platform_device *pdev) +{ + struct max14577_charger *chg; + struct power_supply_config psy_cfg = {}; + struct max14577 *max14577 = dev_get_drvdata(pdev->dev.parent); + int ret; + + chg = devm_kzalloc(&pdev->dev, sizeof(*chg), GFP_KERNEL); + if (!chg) + return -ENOMEM; + + platform_set_drvdata(pdev, chg); + chg->dev = &pdev->dev; + chg->max14577 = max14577; + + chg->pdata = max14577_charger_dt_init(pdev); + if (IS_ERR_OR_NULL(chg->pdata)) + return PTR_ERR(chg->pdata); + + ret = max14577_charger_reg_init(chg); + if (ret) + return ret; + + ret = device_create_file(&pdev->dev, &dev_attr_fast_charge_timer); + if (ret) { + dev_err(&pdev->dev, "failed: create sysfs entry\n"); + return ret; + } + + psy_cfg.drv_data = chg; + chg->charger = power_supply_register(&pdev->dev, &max14577_charger_desc, + &psy_cfg); + if (IS_ERR(chg->charger)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + ret = PTR_ERR(chg->charger); + goto err; + } + + /* Check for valid values for charger */ + BUILD_BUG_ON(MAX14577_CHARGER_EOC_CURRENT_LIMIT_MIN + + MAX14577_CHARGER_EOC_CURRENT_LIMIT_STEP * 0xf != + MAX14577_CHARGER_EOC_CURRENT_LIMIT_MAX); + return 0; + +err: + device_remove_file(&pdev->dev, &dev_attr_fast_charge_timer); + + return ret; +} + +static int max14577_charger_remove(struct platform_device *pdev) +{ + struct max14577_charger *chg = platform_get_drvdata(pdev); + + device_remove_file(&pdev->dev, &dev_attr_fast_charge_timer); + power_supply_unregister(chg->charger); + + return 0; +} + +static const struct platform_device_id max14577_charger_id[] = { + { "max14577-charger", MAXIM_DEVICE_TYPE_MAX14577, }, + { "max77836-charger", MAXIM_DEVICE_TYPE_MAX77836, }, + { } +}; +MODULE_DEVICE_TABLE(platform, max14577_charger_id); + +static const struct of_device_id of_max14577_charger_dt_match[] = { + { .compatible = "maxim,max14577-charger", + .data = (void *)MAXIM_DEVICE_TYPE_MAX14577, }, + { .compatible = "maxim,max77836-charger", + .data = (void *)MAXIM_DEVICE_TYPE_MAX77836, }, + { }, +}; +MODULE_DEVICE_TABLE(of, of_max14577_charger_dt_match); + +static struct platform_driver max14577_charger_driver = { + .driver = { + .name = "max14577-charger", + .of_match_table = of_max14577_charger_dt_match, + }, + .probe = max14577_charger_probe, + .remove = max14577_charger_remove, + .id_table = max14577_charger_id, +}; +module_platform_driver(max14577_charger_driver); + +MODULE_AUTHOR("Krzysztof Kozlowski <krzk@kernel.org>"); +MODULE_DESCRIPTION("Maxim 14577/77836 charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max14656_charger_detector.c b/drivers/power/supply/max14656_charger_detector.c new file mode 100644 index 000000000..137f9fafc --- /dev/null +++ b/drivers/power/supply/max14656_charger_detector.c @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Maxim MAX14656 / AL32 USB Charger Detector driver + * + * Copyright (C) 2014 LG Electronics, Inc + * Copyright (C) 2016 Alexander Kurz <akurz@blala.de> + * + * Components from Maxim AL32 Charger detection Driver for MX50 Yoshi Board + * Copyright (C) Amazon Technologies Inc. All rights reserved. + * Manish Lachwani (lachwani@lab126.com) + */ +#include <linux/module.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/slab.h> +#include <linux/gpio.h> +#include <linux/of_gpio.h> +#include <linux/of_device.h> +#include <linux/workqueue.h> +#include <linux/power_supply.h> + +#define MAX14656_MANUFACTURER "Maxim Integrated" +#define MAX14656_NAME "max14656" + +#define MAX14656_DEVICE_ID 0x00 +#define MAX14656_INTERRUPT_1 0x01 +#define MAX14656_INTERRUPT_2 0x02 +#define MAX14656_STATUS_1 0x03 +#define MAX14656_STATUS_2 0x04 +#define MAX14656_INTMASK_1 0x05 +#define MAX14656_INTMASK_2 0x06 +#define MAX14656_CONTROL_1 0x07 +#define MAX14656_CONTROL_2 0x08 +#define MAX14656_CONTROL_3 0x09 + +#define DEVICE_VENDOR_MASK 0xf0 +#define DEVICE_REV_MASK 0x0f +#define INT_EN_REG_MASK BIT(4) +#define CHG_TYPE_INT_MASK BIT(0) +#define STATUS1_VB_VALID_MASK BIT(4) +#define STATUS1_CHG_TYPE_MASK 0xf +#define INT1_DCD_TIMEOUT_MASK BIT(7) +#define CONTROL1_DEFAULT 0x0d +#define CONTROL1_INT_EN BIT(4) +#define CONTROL1_INT_ACTIVE_HIGH BIT(5) +#define CONTROL1_EDGE BIT(7) +#define CONTROL2_DEFAULT 0x8e +#define CONTROL2_ADC_EN BIT(0) +#define CONTROL3_DEFAULT 0x8d + +enum max14656_chg_type { + MAX14656_NO_CHARGER = 0, + MAX14656_SDP_CHARGER, + MAX14656_CDP_CHARGER, + MAX14656_DCP_CHARGER, + MAX14656_APPLE_500MA_CHARGER, + MAX14656_APPLE_1A_CHARGER, + MAX14656_APPLE_2A_CHARGER, + MAX14656_SPECIAL_500MA_CHARGER, + MAX14656_APPLE_12W, + MAX14656_CHARGER_LAST +}; + +static const struct max14656_chg_type_props { + enum power_supply_type type; +} chg_type_props[] = { + { POWER_SUPPLY_TYPE_UNKNOWN }, + { POWER_SUPPLY_TYPE_USB }, + { POWER_SUPPLY_TYPE_USB_CDP }, + { POWER_SUPPLY_TYPE_USB_DCP }, + { POWER_SUPPLY_TYPE_USB_DCP }, + { POWER_SUPPLY_TYPE_USB_DCP }, + { POWER_SUPPLY_TYPE_USB_DCP }, + { POWER_SUPPLY_TYPE_USB_DCP }, + { POWER_SUPPLY_TYPE_USB }, +}; + +struct max14656_chip { + struct i2c_client *client; + struct power_supply *detect_psy; + struct power_supply_desc psy_desc; + struct delayed_work irq_work; + + int irq; + int online; +}; + +static int max14656_read_reg(struct i2c_client *client, int reg, u8 *val) +{ + s32 ret; + + ret = i2c_smbus_read_byte_data(client, reg); + if (ret < 0) { + dev_err(&client->dev, + "i2c read fail: can't read from %02x: %d\n", + reg, ret); + return ret; + } + *val = ret; + return 0; +} + +static int max14656_write_reg(struct i2c_client *client, int reg, u8 val) +{ + s32 ret; + + ret = i2c_smbus_write_byte_data(client, reg, val); + if (ret < 0) { + dev_err(&client->dev, + "i2c write fail: can't write %02x to %02x: %d\n", + val, reg, ret); + return ret; + } + return 0; +} + +static int max14656_read_block_reg(struct i2c_client *client, u8 reg, + u8 length, u8 *val) +{ + int ret; + + ret = i2c_smbus_read_i2c_block_data(client, reg, length, val); + if (ret < 0) { + dev_err(&client->dev, "failed to block read reg 0x%x: %d\n", + reg, ret); + return ret; + } + + return 0; +} + +#define REG_TOTAL_NUM 5 +static void max14656_irq_worker(struct work_struct *work) +{ + struct max14656_chip *chip = + container_of(work, struct max14656_chip, irq_work.work); + + u8 buf[REG_TOTAL_NUM]; + u8 chg_type; + + max14656_read_block_reg(chip->client, MAX14656_DEVICE_ID, + REG_TOTAL_NUM, buf); + + if ((buf[MAX14656_STATUS_1] & STATUS1_VB_VALID_MASK) && + (buf[MAX14656_STATUS_1] & STATUS1_CHG_TYPE_MASK)) { + chg_type = buf[MAX14656_STATUS_1] & STATUS1_CHG_TYPE_MASK; + if (chg_type < MAX14656_CHARGER_LAST) + chip->psy_desc.type = chg_type_props[chg_type].type; + else + chip->psy_desc.type = POWER_SUPPLY_TYPE_UNKNOWN; + chip->online = 1; + } else { + chip->online = 0; + chip->psy_desc.type = POWER_SUPPLY_TYPE_UNKNOWN; + } + + power_supply_changed(chip->detect_psy); +} + +static irqreturn_t max14656_irq(int irq, void *dev_id) +{ + struct max14656_chip *chip = dev_id; + + schedule_delayed_work(&chip->irq_work, msecs_to_jiffies(100)); + + return IRQ_HANDLED; +} + +static int max14656_hw_init(struct max14656_chip *chip) +{ + uint8_t val = 0; + uint8_t rev; + struct i2c_client *client = chip->client; + + if (max14656_read_reg(client, MAX14656_DEVICE_ID, &val)) + return -ENODEV; + + if ((val & DEVICE_VENDOR_MASK) != 0x20) { + dev_err(&client->dev, "wrong vendor ID %d\n", + ((val & DEVICE_VENDOR_MASK) >> 4)); + return -ENODEV; + } + rev = val & DEVICE_REV_MASK; + + /* Turn on ADC_EN */ + if (max14656_write_reg(client, MAX14656_CONTROL_2, CONTROL2_ADC_EN)) + return -EINVAL; + + /* turn on interrupts and low power mode */ + if (max14656_write_reg(client, MAX14656_CONTROL_1, + CONTROL1_DEFAULT | + CONTROL1_INT_EN | + CONTROL1_INT_ACTIVE_HIGH | + CONTROL1_EDGE)) + return -EINVAL; + + if (max14656_write_reg(client, MAX14656_INTMASK_1, 0x3)) + return -EINVAL; + + if (max14656_write_reg(client, MAX14656_INTMASK_2, 0x1)) + return -EINVAL; + + dev_info(&client->dev, "detected revision %d\n", rev); + return 0; +} + +static int max14656_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max14656_chip *chip = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = chip->online; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = MAX14656_NAME; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = MAX14656_MANUFACTURER; + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property max14656_battery_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static void stop_irq_work(void *data) +{ + struct max14656_chip *chip = data; + + cancel_delayed_work_sync(&chip->irq_work); +} + + +static int max14656_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct device *dev = &client->dev; + struct power_supply_config psy_cfg = {}; + struct max14656_chip *chip; + int irq = client->irq; + int ret = 0; + + if (irq <= 0) { + dev_err(dev, "invalid irq number: %d\n", irq); + return -ENODEV; + } + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { + dev_err(dev, "No support for SMBUS_BYTE_DATA\n"); + return -ENODEV; + } + + chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + psy_cfg.drv_data = chip; + chip->client = client; + chip->online = 0; + chip->psy_desc.name = MAX14656_NAME; + chip->psy_desc.type = POWER_SUPPLY_TYPE_UNKNOWN; + chip->psy_desc.properties = max14656_battery_props; + chip->psy_desc.num_properties = ARRAY_SIZE(max14656_battery_props); + chip->psy_desc.get_property = max14656_get_property; + chip->irq = irq; + + ret = max14656_hw_init(chip); + if (ret) + return -ENODEV; + + chip->detect_psy = devm_power_supply_register(dev, + &chip->psy_desc, &psy_cfg); + if (IS_ERR(chip->detect_psy)) { + dev_err(dev, "power_supply_register failed\n"); + return -EINVAL; + } + + INIT_DELAYED_WORK(&chip->irq_work, max14656_irq_worker); + ret = devm_add_action(dev, stop_irq_work, chip); + if (ret) { + dev_err(dev, "devm_add_action %d failed\n", ret); + return ret; + } + + ret = devm_request_irq(dev, chip->irq, max14656_irq, + IRQF_TRIGGER_FALLING, + MAX14656_NAME, chip); + if (ret) { + dev_err(dev, "request_irq %d failed\n", chip->irq); + return -EINVAL; + } + enable_irq_wake(chip->irq); + + schedule_delayed_work(&chip->irq_work, msecs_to_jiffies(2000)); + + return 0; +} + +static const struct i2c_device_id max14656_id[] = { + { "max14656", 0 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, max14656_id); + +static const struct of_device_id max14656_match_table[] = { + { .compatible = "maxim,max14656", }, + {} +}; +MODULE_DEVICE_TABLE(of, max14656_match_table); + +static struct i2c_driver max14656_i2c_driver = { + .driver = { + .name = "max14656", + .of_match_table = max14656_match_table, + }, + .probe = max14656_probe, + .id_table = max14656_id, +}; +module_i2c_driver(max14656_i2c_driver); + +MODULE_DESCRIPTION("MAX14656 USB charger detector"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/max17040_battery.c b/drivers/power/supply/max17040_battery.c new file mode 100644 index 000000000..b6b29ec3d --- /dev/null +++ b/drivers/power/supply/max17040_battery.c @@ -0,0 +1,643 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// max17040_battery.c +// fuel-gauge systems for lithium-ion (Li+) batteries +// +// Copyright (C) 2009 Samsung Electronics +// Minkyu Kang <mk7.kang@samsung.com> + +#include <linux/module.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/mutex.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/power_supply.h> +#include <linux/of_device.h> +#include <linux/max17040_battery.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#define MAX17040_VCELL 0x02 +#define MAX17040_SOC 0x04 +#define MAX17040_MODE 0x06 +#define MAX17040_VER 0x08 +#define MAX17040_CONFIG 0x0C +#define MAX17040_STATUS 0x1A +#define MAX17040_CMD 0xFE + + +#define MAX17040_DELAY 1000 +#define MAX17040_BATTERY_FULL 95 +#define MAX17040_RCOMP_DEFAULT 0x9700 + +#define MAX17040_ATHD_MASK 0x3f +#define MAX17040_ALSC_MASK 0x40 +#define MAX17040_ATHD_DEFAULT_POWER_UP 4 +#define MAX17040_STATUS_HD_MASK 0x1000 +#define MAX17040_STATUS_SC_MASK 0x2000 +#define MAX17040_CFG_RCOMP_MASK 0xff00 + +enum chip_id { + ID_MAX17040, + ID_MAX17041, + ID_MAX17043, + ID_MAX17044, + ID_MAX17048, + ID_MAX17049, + ID_MAX17058, + ID_MAX17059, +}; + +/* values that differ by chip_id */ +struct chip_data { + u16 reset_val; + u16 vcell_shift; + u16 vcell_mul; + u16 vcell_div; + u8 has_low_soc_alert; + u8 rcomp_bytes; + u8 has_soc_alert; +}; + +static struct chip_data max17040_family[] = { + [ID_MAX17040] = { + .reset_val = 0x0054, + .vcell_shift = 4, + .vcell_mul = 1250, + .vcell_div = 1, + .has_low_soc_alert = 0, + .rcomp_bytes = 2, + .has_soc_alert = 0, + }, + [ID_MAX17041] = { + .reset_val = 0x0054, + .vcell_shift = 4, + .vcell_mul = 2500, + .vcell_div = 1, + .has_low_soc_alert = 0, + .rcomp_bytes = 2, + .has_soc_alert = 0, + }, + [ID_MAX17043] = { + .reset_val = 0x0054, + .vcell_shift = 4, + .vcell_mul = 1250, + .vcell_div = 1, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 0, + }, + [ID_MAX17044] = { + .reset_val = 0x0054, + .vcell_shift = 4, + .vcell_mul = 2500, + .vcell_div = 1, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 0, + }, + [ID_MAX17048] = { + .reset_val = 0x5400, + .vcell_shift = 0, + .vcell_mul = 625, + .vcell_div = 8, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 1, + }, + [ID_MAX17049] = { + .reset_val = 0x5400, + .vcell_shift = 0, + .vcell_mul = 625, + .vcell_div = 4, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 1, + }, + [ID_MAX17058] = { + .reset_val = 0x5400, + .vcell_shift = 0, + .vcell_mul = 625, + .vcell_div = 8, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 0, + }, + [ID_MAX17059] = { + .reset_val = 0x5400, + .vcell_shift = 0, + .vcell_mul = 625, + .vcell_div = 4, + .has_low_soc_alert = 1, + .rcomp_bytes = 1, + .has_soc_alert = 0, + }, +}; + +struct max17040_chip { + struct i2c_client *client; + struct regmap *regmap; + struct delayed_work work; + struct power_supply *battery; + struct max17040_platform_data *pdata; + struct chip_data data; + + /* battery capacity */ + int soc; + /* State Of Charge */ + int status; + /* Low alert threshold from 32% to 1% of the State of Charge */ + u32 low_soc_alert; + /* some devices return twice the capacity */ + bool quirk_double_soc; + /* higher 8 bits for 17043+, 16 bits for 17040,41 */ + u16 rcomp; +}; + +static int max17040_reset(struct max17040_chip *chip) +{ + return regmap_write(chip->regmap, MAX17040_CMD, chip->data.reset_val); +} + +static int max17040_set_low_soc_alert(struct max17040_chip *chip, u32 level) +{ + level = 32 - level * (chip->quirk_double_soc ? 2 : 1); + return regmap_update_bits(chip->regmap, MAX17040_CONFIG, + MAX17040_ATHD_MASK, level); +} + +static int max17040_set_soc_alert(struct max17040_chip *chip, bool enable) +{ + return regmap_update_bits(chip->regmap, MAX17040_CONFIG, + MAX17040_ALSC_MASK, enable ? MAX17040_ALSC_MASK : 0); +} + +static int max17040_set_rcomp(struct max17040_chip *chip, u16 rcomp) +{ + u16 mask = chip->data.rcomp_bytes == 2 ? + 0xffff : MAX17040_CFG_RCOMP_MASK; + + return regmap_update_bits(chip->regmap, MAX17040_CONFIG, mask, rcomp); +} + +static int max17040_raw_vcell_to_uvolts(struct max17040_chip *chip, u16 vcell) +{ + struct chip_data *d = &chip->data; + + return (vcell >> d->vcell_shift) * d->vcell_mul / d->vcell_div; +} + + +static int max17040_get_vcell(struct max17040_chip *chip) +{ + u32 vcell; + + regmap_read(chip->regmap, MAX17040_VCELL, &vcell); + + return max17040_raw_vcell_to_uvolts(chip, vcell); +} + +static int max17040_get_soc(struct max17040_chip *chip) +{ + u32 soc; + + regmap_read(chip->regmap, MAX17040_SOC, &soc); + + return soc >> (chip->quirk_double_soc ? 9 : 8); +} + +static int max17040_get_version(struct max17040_chip *chip) +{ + int ret; + u32 version; + + ret = regmap_read(chip->regmap, MAX17040_VER, &version); + + return ret ? ret : version; +} + +static int max17040_get_online(struct max17040_chip *chip) +{ + return chip->pdata && chip->pdata->battery_online ? + chip->pdata->battery_online() : 1; +} + +static int max17040_get_status(struct max17040_chip *chip) +{ + if (!chip->pdata || !chip->pdata->charger_online + || !chip->pdata->charger_enable) + return POWER_SUPPLY_STATUS_UNKNOWN; + + if (max17040_get_soc(chip) > MAX17040_BATTERY_FULL) + return POWER_SUPPLY_STATUS_FULL; + + if (chip->pdata->charger_online()) + if (chip->pdata->charger_enable()) + return POWER_SUPPLY_STATUS_CHARGING; + else + return POWER_SUPPLY_STATUS_NOT_CHARGING; + else + return POWER_SUPPLY_STATUS_DISCHARGING; +} + +static int max17040_get_of_data(struct max17040_chip *chip) +{ + struct device *dev = &chip->client->dev; + struct chip_data *data = &max17040_family[ + (uintptr_t) of_device_get_match_data(dev)]; + int rcomp_len; + u8 rcomp[2]; + + chip->quirk_double_soc = device_property_read_bool(dev, + "maxim,double-soc"); + + chip->low_soc_alert = MAX17040_ATHD_DEFAULT_POWER_UP; + device_property_read_u32(dev, + "maxim,alert-low-soc-level", + &chip->low_soc_alert); + + if (chip->low_soc_alert <= 0 || + chip->low_soc_alert > (chip->quirk_double_soc ? 16 : 32)) { + dev_err(dev, "maxim,alert-low-soc-level out of bounds\n"); + return -EINVAL; + } + + rcomp_len = device_property_count_u8(dev, "maxim,rcomp"); + chip->rcomp = MAX17040_RCOMP_DEFAULT; + if (rcomp_len == data->rcomp_bytes) { + device_property_read_u8_array(dev, "maxim,rcomp", + rcomp, rcomp_len); + chip->rcomp = rcomp_len == 2 ? + rcomp[0] << 8 | rcomp[1] : + rcomp[0] << 8; + } else if (rcomp_len > 0) { + dev_err(dev, "maxim,rcomp has incorrect length\n"); + return -EINVAL; + } + + return 0; +} + +static void max17040_check_changes(struct max17040_chip *chip) +{ + chip->soc = max17040_get_soc(chip); + chip->status = max17040_get_status(chip); +} + +static void max17040_queue_work(struct max17040_chip *chip) +{ + queue_delayed_work(system_power_efficient_wq, &chip->work, + MAX17040_DELAY); +} + +static void max17040_stop_work(void *data) +{ + struct max17040_chip *chip = data; + + cancel_delayed_work_sync(&chip->work); +} + +static void max17040_work(struct work_struct *work) +{ + struct max17040_chip *chip; + int last_soc, last_status; + + chip = container_of(work, struct max17040_chip, work.work); + + /* store SOC and status to check changes */ + last_soc = chip->soc; + last_status = chip->status; + max17040_check_changes(chip); + + /* check changes and send uevent */ + if (last_soc != chip->soc || last_status != chip->status) + power_supply_changed(chip->battery); + + max17040_queue_work(chip); +} + +/* Returns true if alert cause was SOC change, not low SOC */ +static bool max17040_handle_soc_alert(struct max17040_chip *chip) +{ + bool ret = true; + u32 data; + + regmap_read(chip->regmap, MAX17040_STATUS, &data); + + if (data & MAX17040_STATUS_HD_MASK) { + // this alert was caused by low soc + ret = false; + } + if (data & MAX17040_STATUS_SC_MASK) { + // soc change bit -- deassert to mark as handled + regmap_write(chip->regmap, MAX17040_STATUS, + data & ~MAX17040_STATUS_SC_MASK); + } + + return ret; +} + +static irqreturn_t max17040_thread_handler(int id, void *dev) +{ + struct max17040_chip *chip = dev; + + if (!(chip->data.has_soc_alert && max17040_handle_soc_alert(chip))) + dev_warn(&chip->client->dev, "IRQ: Alert battery low level\n"); + + /* read registers */ + max17040_check_changes(chip); + + /* send uevent */ + power_supply_changed(chip->battery); + + /* reset alert bit */ + max17040_set_low_soc_alert(chip, chip->low_soc_alert); + + return IRQ_HANDLED; +} + +static int max17040_enable_alert_irq(struct max17040_chip *chip) +{ + struct i2c_client *client = chip->client; + unsigned int flags; + int ret; + + flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT; + ret = devm_request_threaded_irq(&client->dev, client->irq, NULL, + max17040_thread_handler, flags, + chip->battery->desc->name, chip); + + return ret; +} + +static int max17040_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + return 1; + default: + return 0; + } +} + +static int max17040_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct max17040_chip *chip = power_supply_get_drvdata(psy); + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + /* alert threshold can be programmed from 1% up to 16/32% */ + if ((val->intval < 1) || + (val->intval > (chip->quirk_double_soc ? 16 : 32))) { + ret = -EINVAL; + break; + } + ret = max17040_set_low_soc_alert(chip, val->intval); + chip->low_soc_alert = val->intval; + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static int max17040_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max17040_chip *chip = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = max17040_get_status(chip); + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = max17040_get_online(chip); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = max17040_get_vcell(chip); + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = max17040_get_soc(chip); + break; + case POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN: + val->intval = chip->low_soc_alert; + break; + default: + return -EINVAL; + } + return 0; +} + +static const struct regmap_config max17040_regmap = { + .reg_bits = 8, + .reg_stride = 2, + .val_bits = 16, + .val_format_endian = REGMAP_ENDIAN_BIG, +}; + +static enum power_supply_property max17040_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN, +}; + +static const struct power_supply_desc max17040_battery_desc = { + .name = "battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = max17040_get_property, + .set_property = max17040_set_property, + .property_is_writeable = max17040_prop_writeable, + .properties = max17040_battery_props, + .num_properties = ARRAY_SIZE(max17040_battery_props), +}; + +static int max17040_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct power_supply_config psy_cfg = {}; + struct max17040_chip *chip; + enum chip_id chip_id; + bool enable_irq = false; + int ret; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE)) + return -EIO; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->client = client; + chip->regmap = devm_regmap_init_i2c(client, &max17040_regmap); + chip->pdata = client->dev.platform_data; + if (IS_ERR(chip->regmap)) + return PTR_ERR(chip->regmap); + chip_id = (enum chip_id) id->driver_data; + if (client->dev.of_node) { + ret = max17040_get_of_data(chip); + if (ret) + return ret; + chip_id = (enum chip_id) (uintptr_t) + of_device_get_match_data(&client->dev); + } + chip->data = max17040_family[chip_id]; + + i2c_set_clientdata(client, chip); + psy_cfg.drv_data = chip; + + chip->battery = devm_power_supply_register(&client->dev, + &max17040_battery_desc, &psy_cfg); + if (IS_ERR(chip->battery)) { + dev_err(&client->dev, "failed: power supply register\n"); + return PTR_ERR(chip->battery); + } + + ret = max17040_get_version(chip); + if (ret < 0) + return ret; + dev_dbg(&chip->client->dev, "MAX17040 Fuel-Gauge Ver 0x%x\n", ret); + + if (chip_id == ID_MAX17040 || chip_id == ID_MAX17041) + max17040_reset(chip); + + max17040_set_rcomp(chip, chip->rcomp); + + /* check interrupt */ + if (client->irq && chip->data.has_low_soc_alert) { + ret = max17040_set_low_soc_alert(chip, chip->low_soc_alert); + if (ret) { + dev_err(&client->dev, + "Failed to set low SOC alert: err %d\n", ret); + return ret; + } + + enable_irq = true; + } + + if (client->irq && chip->data.has_soc_alert) { + ret = max17040_set_soc_alert(chip, 1); + if (ret) { + dev_err(&client->dev, + "Failed to set SOC alert: err %d\n", ret); + return ret; + } + enable_irq = true; + } else { + /* soc alerts negate the need for polling */ + INIT_DEFERRABLE_WORK(&chip->work, max17040_work); + ret = devm_add_action(&client->dev, max17040_stop_work, chip); + if (ret) + return ret; + max17040_queue_work(chip); + } + + if (enable_irq) { + ret = max17040_enable_alert_irq(chip); + if (ret) { + client->irq = 0; + dev_warn(&client->dev, + "Failed to get IRQ err %d\n", ret); + } + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP + +static int max17040_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct max17040_chip *chip = i2c_get_clientdata(client); + + if (client->irq && chip->data.has_soc_alert) + // disable soc alert to prevent wakeup + max17040_set_soc_alert(chip, 0); + else + cancel_delayed_work(&chip->work); + + if (client->irq && device_may_wakeup(dev)) + enable_irq_wake(client->irq); + + return 0; +} + +static int max17040_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct max17040_chip *chip = i2c_get_clientdata(client); + + if (client->irq && device_may_wakeup(dev)) + disable_irq_wake(client->irq); + + if (client->irq && chip->data.has_soc_alert) + max17040_set_soc_alert(chip, 1); + else + max17040_queue_work(chip); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(max17040_pm_ops, max17040_suspend, max17040_resume); +#define MAX17040_PM_OPS (&max17040_pm_ops) + +#else + +#define MAX17040_PM_OPS NULL + +#endif /* CONFIG_PM_SLEEP */ + +static const struct i2c_device_id max17040_id[] = { + { "max17040", ID_MAX17040 }, + { "max17041", ID_MAX17041 }, + { "max17043", ID_MAX17043 }, + { "max77836-battery", ID_MAX17043 }, + { "max17044", ID_MAX17044 }, + { "max17048", ID_MAX17048 }, + { "max17049", ID_MAX17049 }, + { "max17058", ID_MAX17058 }, + { "max17059", ID_MAX17059 }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(i2c, max17040_id); + +static const struct of_device_id max17040_of_match[] = { + { .compatible = "maxim,max17040", .data = (void *) ID_MAX17040 }, + { .compatible = "maxim,max17041", .data = (void *) ID_MAX17041 }, + { .compatible = "maxim,max17043", .data = (void *) ID_MAX17043 }, + { .compatible = "maxim,max77836-battery", .data = (void *) ID_MAX17043 }, + { .compatible = "maxim,max17044", .data = (void *) ID_MAX17044 }, + { .compatible = "maxim,max17048", .data = (void *) ID_MAX17048 }, + { .compatible = "maxim,max17049", .data = (void *) ID_MAX17049 }, + { .compatible = "maxim,max17058", .data = (void *) ID_MAX17058 }, + { .compatible = "maxim,max17059", .data = (void *) ID_MAX17059 }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, max17040_of_match); + +static struct i2c_driver max17040_i2c_driver = { + .driver = { + .name = "max17040", + .of_match_table = max17040_of_match, + .pm = MAX17040_PM_OPS, + }, + .probe = max17040_probe, + .id_table = max17040_id, +}; +module_i2c_driver(max17040_i2c_driver); + +MODULE_AUTHOR("Minkyu Kang <mk7.kang@samsung.com>"); +MODULE_DESCRIPTION("MAX17040 Fuel Gauge"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c new file mode 100644 index 000000000..76b0f45a2 --- /dev/null +++ b/drivers/power/supply/max17042_battery.c @@ -0,0 +1,1229 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Fuel gauge driver for Maxim 17042 / 8966 / 8997 +// Note that Maxim 8966 and 8997 are mfd and this is its subdevice. +// +// Copyright (C) 2011 Samsung Electronics +// MyungJoo Ham <myungjoo.ham@samsung.com> +// +// This driver is based on max17040_battery.c + +#include <linux/acpi.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/pm.h> +#include <linux/mod_devicetable.h> +#include <linux/power_supply.h> +#include <linux/power/max17042_battery.h> +#include <linux/of.h> +#include <linux/regmap.h> + +/* Status register bits */ +#define STATUS_POR_BIT (1 << 1) +#define STATUS_BST_BIT (1 << 3) +#define STATUS_VMN_BIT (1 << 8) +#define STATUS_TMN_BIT (1 << 9) +#define STATUS_SMN_BIT (1 << 10) +#define STATUS_BI_BIT (1 << 11) +#define STATUS_VMX_BIT (1 << 12) +#define STATUS_TMX_BIT (1 << 13) +#define STATUS_SMX_BIT (1 << 14) +#define STATUS_BR_BIT (1 << 15) + +/* Interrupt mask bits */ +#define CONFIG_ALRT_BIT_ENBL (1 << 2) +#define STATUS_INTR_SOCMIN_BIT (1 << 10) +#define STATUS_INTR_SOCMAX_BIT (1 << 14) + +#define VFSOC0_LOCK 0x0000 +#define VFSOC0_UNLOCK 0x0080 +#define MODEL_UNLOCK1 0X0059 +#define MODEL_UNLOCK2 0X00C4 +#define MODEL_LOCK1 0X0000 +#define MODEL_LOCK2 0X0000 + +#define dQ_ACC_DIV 0x4 +#define dP_ACC_100 0x1900 +#define dP_ACC_200 0x3200 + +#define MAX17042_VMAX_TOLERANCE 50 /* 50 mV */ + +struct max17042_chip { + struct i2c_client *client; + struct regmap *regmap; + struct power_supply *battery; + enum max170xx_chip_type chip_type; + struct max17042_platform_data *pdata; + struct work_struct work; + int init_complete; +}; + +static enum power_supply_property max17042_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_VOLTAGE_OCV, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TEMP_ALERT_MIN, + POWER_SUPPLY_PROP_TEMP_ALERT_MAX, + POWER_SUPPLY_PROP_TEMP_MIN, + POWER_SUPPLY_PROP_TEMP_MAX, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + // these two have to be at the end on the list + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, +}; + +static int max17042_get_temperature(struct max17042_chip *chip, int *temp) +{ + int ret; + u32 data; + struct regmap *map = chip->regmap; + + ret = regmap_read(map, MAX17042_TEMP, &data); + if (ret < 0) + return ret; + + *temp = sign_extend32(data, 15); + /* The value is converted into deci-centigrade scale */ + /* Units of LSB = 1 / 256 degree Celsius */ + *temp = *temp * 10 / 256; + return 0; +} + +static int max17042_get_status(struct max17042_chip *chip, int *status) +{ + int ret, charge_full, charge_now; + int avg_current; + u32 data; + + ret = power_supply_am_i_supplied(chip->battery); + if (ret < 0) { + *status = POWER_SUPPLY_STATUS_UNKNOWN; + return 0; + } + if (ret == 0) { + *status = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + /* + * The MAX170xx has builtin end-of-charge detection and will update + * FullCAP to match RepCap when it detects end of charging. + * + * When this cycle the battery gets charged to a higher (calculated) + * capacity then the previous cycle then FullCAP will get updated + * contineously once end-of-charge detection kicks in, so allow the + * 2 to differ a bit. + */ + + ret = regmap_read(chip->regmap, MAX17042_FullCAP, &charge_full); + if (ret < 0) + return ret; + + ret = regmap_read(chip->regmap, MAX17042_RepCap, &charge_now); + if (ret < 0) + return ret; + + if ((charge_full - charge_now) <= MAX17042_FULL_THRESHOLD) { + *status = POWER_SUPPLY_STATUS_FULL; + return 0; + } + + /* + * Even though we are supplied, we may still be discharging if the + * supply is e.g. only delivering 5V 0.5A. Check current if available. + */ + if (!chip->pdata->enable_current_sense) { + *status = POWER_SUPPLY_STATUS_CHARGING; + return 0; + } + + ret = regmap_read(chip->regmap, MAX17042_AvgCurrent, &data); + if (ret < 0) + return ret; + + avg_current = sign_extend32(data, 15); + avg_current *= 1562500 / chip->pdata->r_sns; + + if (avg_current > 0) + *status = POWER_SUPPLY_STATUS_CHARGING; + else + *status = POWER_SUPPLY_STATUS_DISCHARGING; + + return 0; +} + +static int max17042_get_battery_health(struct max17042_chip *chip, int *health) +{ + int temp, vavg, vbatt, ret; + u32 val; + + ret = regmap_read(chip->regmap, MAX17042_AvgVCELL, &val); + if (ret < 0) + goto health_error; + + /* bits [0-3] unused */ + vavg = val * 625 / 8; + /* Convert to millivolts */ + vavg /= 1000; + + ret = regmap_read(chip->regmap, MAX17042_VCELL, &val); + if (ret < 0) + goto health_error; + + /* bits [0-3] unused */ + vbatt = val * 625 / 8; + /* Convert to millivolts */ + vbatt /= 1000; + + if (vavg < chip->pdata->vmin) { + *health = POWER_SUPPLY_HEALTH_DEAD; + goto out; + } + + if (vbatt > chip->pdata->vmax + MAX17042_VMAX_TOLERANCE) { + *health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + goto out; + } + + ret = max17042_get_temperature(chip, &temp); + if (ret < 0) + goto health_error; + + if (temp < chip->pdata->temp_min) { + *health = POWER_SUPPLY_HEALTH_COLD; + goto out; + } + + if (temp > chip->pdata->temp_max) { + *health = POWER_SUPPLY_HEALTH_OVERHEAT; + goto out; + } + + *health = POWER_SUPPLY_HEALTH_GOOD; + +out: + return 0; + +health_error: + return ret; +} + +static int max17042_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max17042_chip *chip = power_supply_get_drvdata(psy); + struct regmap *map = chip->regmap; + int ret; + u32 data; + u64 data64; + + if (!chip->init_complete) + return -EAGAIN; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = max17042_get_status(chip, &val->intval); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = regmap_read(map, MAX17042_STATUS, &data); + if (ret < 0) + return ret; + + if (data & MAX17042_STATUS_BattAbsent) + val->intval = 0; + else + val->intval = 1; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + ret = regmap_read(map, MAX17042_Cycles, &data); + if (ret < 0) + return ret; + + val->intval = data; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + ret = regmap_read(map, MAX17042_MinMaxVolt, &data); + if (ret < 0) + return ret; + + val->intval = data >> 8; + val->intval *= 20000; /* Units of LSB = 20mV */ + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + ret = regmap_read(map, MAX17042_MinMaxVolt, &data); + if (ret < 0) + return ret; + + val->intval = (data & 0xff) * 20000; /* Units of 20mV */ + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) + ret = regmap_read(map, MAX17042_V_empty, &data); + else if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055) + ret = regmap_read(map, MAX17055_V_empty, &data); + else + ret = regmap_read(map, MAX17047_V_empty, &data); + if (ret < 0) + return ret; + + val->intval = data >> 7; + val->intval *= 10000; /* Units of LSB = 10mV */ + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = regmap_read(map, MAX17042_VCELL, &data); + if (ret < 0) + return ret; + + val->intval = data * 625 / 8; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + ret = regmap_read(map, MAX17042_AvgVCELL, &data); + if (ret < 0) + return ret; + + val->intval = data * 625 / 8; + break; + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + ret = regmap_read(map, MAX17042_OCVInternal, &data); + if (ret < 0) + return ret; + + val->intval = data * 625 / 8; + break; + case POWER_SUPPLY_PROP_CAPACITY: + if (chip->pdata->enable_current_sense) + ret = regmap_read(map, MAX17042_RepSOC, &data); + else + ret = regmap_read(map, MAX17042_VFSOC, &data); + if (ret < 0) + return ret; + + val->intval = data >> 8; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = regmap_read(map, MAX17042_DesignCap, &data); + if (ret < 0) + return ret; + + data64 = data * 5000000ll; + do_div(data64, chip->pdata->r_sns); + val->intval = data64; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = regmap_read(map, MAX17042_FullCAP, &data); + if (ret < 0) + return ret; + + data64 = data * 5000000ll; + do_div(data64, chip->pdata->r_sns); + val->intval = data64; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = regmap_read(map, MAX17042_RepCap, &data); + if (ret < 0) + return ret; + + data64 = data * 5000000ll; + do_div(data64, chip->pdata->r_sns); + val->intval = data64; + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + ret = regmap_read(map, MAX17042_QH, &data); + if (ret < 0) + return ret; + + val->intval = data * 1000 / 2; + break; + case POWER_SUPPLY_PROP_TEMP: + ret = max17042_get_temperature(chip, &val->intval); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + ret = regmap_read(map, MAX17042_TALRT_Th, &data); + if (ret < 0) + return ret; + /* LSB is Alert Minimum. In deci-centigrade */ + val->intval = sign_extend32(data & 0xff, 7) * 10; + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = regmap_read(map, MAX17042_TALRT_Th, &data); + if (ret < 0) + return ret; + /* MSB is Alert Maximum. In deci-centigrade */ + val->intval = sign_extend32(data >> 8, 7) * 10; + break; + case POWER_SUPPLY_PROP_TEMP_MIN: + val->intval = chip->pdata->temp_min; + break; + case POWER_SUPPLY_PROP_TEMP_MAX: + val->intval = chip->pdata->temp_max; + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = max17042_get_battery_health(chip, &val->intval); + if (ret < 0) + return ret; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (chip->pdata->enable_current_sense) { + ret = regmap_read(map, MAX17042_Current, &data); + if (ret < 0) + return ret; + + val->intval = sign_extend32(data, 15); + val->intval *= 1562500 / chip->pdata->r_sns; + } else { + return -EINVAL; + } + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + if (chip->pdata->enable_current_sense) { + ret = regmap_read(map, MAX17042_AvgCurrent, &data); + if (ret < 0) + return ret; + + val->intval = sign_extend32(data, 15); + val->intval *= 1562500 / chip->pdata->r_sns; + } else { + return -EINVAL; + } + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + ret = regmap_read(map, MAX17042_TTE, &data); + if (ret < 0) + return ret; + + val->intval = data * 5625 / 1000; + break; + default: + return -EINVAL; + } + return 0; +} + +static int max17042_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct max17042_chip *chip = power_supply_get_drvdata(psy); + struct regmap *map = chip->regmap; + int ret = 0; + u32 data; + int8_t temp; + + switch (psp) { + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + ret = regmap_read(map, MAX17042_TALRT_Th, &data); + if (ret < 0) + return ret; + + /* Input in deci-centigrade, convert to centigrade */ + temp = val->intval / 10; + /* force min < max */ + if (temp >= (int8_t)(data >> 8)) + temp = (int8_t)(data >> 8) - 1; + /* Write both MAX and MIN ALERT */ + data = (data & 0xff00) + temp; + ret = regmap_write(map, MAX17042_TALRT_Th, data); + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = regmap_read(map, MAX17042_TALRT_Th, &data); + if (ret < 0) + return ret; + + /* Input in Deci-Centigrade, convert to centigrade */ + temp = val->intval / 10; + /* force max > min */ + if (temp <= (int8_t)(data & 0xff)) + temp = (int8_t)(data & 0xff) + 1; + /* Write both MAX and MIN ALERT */ + data = (data & 0xff) + (temp << 8); + ret = regmap_write(map, MAX17042_TALRT_Th, data); + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static int max17042_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} + +static void max17042_external_power_changed(struct power_supply *psy) +{ + power_supply_changed(psy); +} + +static int max17042_write_verify_reg(struct regmap *map, u8 reg, u32 value) +{ + int retries = 8; + int ret; + u32 read_value; + + do { + ret = regmap_write(map, reg, value); + regmap_read(map, reg, &read_value); + if (read_value != value) { + ret = -EIO; + retries--; + } + } while (retries && read_value != value); + + if (ret < 0) + pr_err("%s: err %d\n", __func__, ret); + + return ret; +} + +static inline void max17042_override_por(struct regmap *map, + u8 reg, u16 value) +{ + if (value) + regmap_write(map, reg, value); +} + +static inline void max17042_unlock_model(struct max17042_chip *chip) +{ + struct regmap *map = chip->regmap; + + regmap_write(map, MAX17042_MLOCKReg1, MODEL_UNLOCK1); + regmap_write(map, MAX17042_MLOCKReg2, MODEL_UNLOCK2); +} + +static inline void max17042_lock_model(struct max17042_chip *chip) +{ + struct regmap *map = chip->regmap; + + regmap_write(map, MAX17042_MLOCKReg1, MODEL_LOCK1); + regmap_write(map, MAX17042_MLOCKReg2, MODEL_LOCK2); +} + +static inline void max17042_write_model_data(struct max17042_chip *chip, + u8 addr, int size) +{ + struct regmap *map = chip->regmap; + int i; + + for (i = 0; i < size; i++) + regmap_write(map, addr + i, + chip->pdata->config_data->cell_char_tbl[i]); +} + +static inline void max17042_read_model_data(struct max17042_chip *chip, + u8 addr, u16 *data, int size) +{ + struct regmap *map = chip->regmap; + int i; + u32 tmp; + + for (i = 0; i < size; i++) { + regmap_read(map, addr + i, &tmp); + data[i] = (u16)tmp; + } +} + +static inline int max17042_model_data_compare(struct max17042_chip *chip, + u16 *data1, u16 *data2, int size) +{ + int i; + + if (memcmp(data1, data2, size)) { + dev_err(&chip->client->dev, "%s compare failed\n", __func__); + for (i = 0; i < size; i++) + dev_info(&chip->client->dev, "0x%x, 0x%x", + data1[i], data2[i]); + dev_info(&chip->client->dev, "\n"); + return -EINVAL; + } + return 0; +} + +static int max17042_init_model(struct max17042_chip *chip) +{ + int ret; + int table_size = ARRAY_SIZE(chip->pdata->config_data->cell_char_tbl); + u16 *temp_data; + + temp_data = kcalloc(table_size, sizeof(*temp_data), GFP_KERNEL); + if (!temp_data) + return -ENOMEM; + + max17042_unlock_model(chip); + max17042_write_model_data(chip, MAX17042_MODELChrTbl, + table_size); + max17042_read_model_data(chip, MAX17042_MODELChrTbl, temp_data, + table_size); + + ret = max17042_model_data_compare( + chip, + chip->pdata->config_data->cell_char_tbl, + temp_data, + table_size); + + max17042_lock_model(chip); + kfree(temp_data); + + return ret; +} + +static int max17042_verify_model_lock(struct max17042_chip *chip) +{ + int i; + int table_size = ARRAY_SIZE(chip->pdata->config_data->cell_char_tbl); + u16 *temp_data; + int ret = 0; + + temp_data = kcalloc(table_size, sizeof(*temp_data), GFP_KERNEL); + if (!temp_data) + return -ENOMEM; + + max17042_read_model_data(chip, MAX17042_MODELChrTbl, temp_data, + table_size); + for (i = 0; i < table_size; i++) + if (temp_data[i]) + ret = -EINVAL; + + kfree(temp_data); + return ret; +} + +static void max17042_write_config_regs(struct max17042_chip *chip) +{ + struct max17042_config_data *config = chip->pdata->config_data; + struct regmap *map = chip->regmap; + + regmap_write(map, MAX17042_CONFIG, config->config); + regmap_write(map, MAX17042_LearnCFG, config->learn_cfg); + regmap_write(map, MAX17042_FilterCFG, + config->filter_cfg); + regmap_write(map, MAX17042_RelaxCFG, config->relax_cfg); + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047 || + chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050 || + chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055) + regmap_write(map, MAX17047_FullSOCThr, + config->full_soc_thresh); +} + +static void max17042_write_custom_regs(struct max17042_chip *chip) +{ + struct max17042_config_data *config = chip->pdata->config_data; + struct regmap *map = chip->regmap; + + max17042_write_verify_reg(map, MAX17042_RCOMP0, config->rcomp0); + max17042_write_verify_reg(map, MAX17042_TempCo, config->tcompc0); + max17042_write_verify_reg(map, MAX17042_ICHGTerm, config->ichgt_term); + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) { + regmap_write(map, MAX17042_EmptyTempCo, config->empty_tempco); + max17042_write_verify_reg(map, MAX17042_K_empty0, + config->kempty0); + } else { + max17042_write_verify_reg(map, MAX17047_QRTbl00, + config->qrtbl00); + max17042_write_verify_reg(map, MAX17047_QRTbl10, + config->qrtbl10); + max17042_write_verify_reg(map, MAX17047_QRTbl20, + config->qrtbl20); + max17042_write_verify_reg(map, MAX17047_QRTbl30, + config->qrtbl30); + } +} + +static void max17042_update_capacity_regs(struct max17042_chip *chip) +{ + struct max17042_config_data *config = chip->pdata->config_data; + struct regmap *map = chip->regmap; + + max17042_write_verify_reg(map, MAX17042_FullCAP, + config->fullcap); + regmap_write(map, MAX17042_DesignCap, config->design_cap); + max17042_write_verify_reg(map, MAX17042_FullCAPNom, + config->fullcapnom); +} + +static void max17042_reset_vfsoc0_reg(struct max17042_chip *chip) +{ + unsigned int vfSoc; + struct regmap *map = chip->regmap; + + regmap_read(map, MAX17042_VFSOC, &vfSoc); + regmap_write(map, MAX17042_VFSOC0Enable, VFSOC0_UNLOCK); + max17042_write_verify_reg(map, MAX17042_VFSOC0, vfSoc); + regmap_write(map, MAX17042_VFSOC0Enable, VFSOC0_LOCK); +} + +static void max17042_load_new_capacity_params(struct max17042_chip *chip) +{ + u32 full_cap0, rep_cap, dq_acc, vfSoc; + u32 rem_cap; + + struct max17042_config_data *config = chip->pdata->config_data; + struct regmap *map = chip->regmap; + + regmap_read(map, MAX17042_FullCAP0, &full_cap0); + regmap_read(map, MAX17042_VFSOC, &vfSoc); + + /* fg_vfSoc needs to shifted by 8 bits to get the + * perc in 1% accuracy, to get the right rem_cap multiply + * full_cap0, fg_vfSoc and devide by 100 + */ + rem_cap = ((vfSoc >> 8) * full_cap0) / 100; + max17042_write_verify_reg(map, MAX17042_RemCap, rem_cap); + + rep_cap = rem_cap; + max17042_write_verify_reg(map, MAX17042_RepCap, rep_cap); + + /* Write dQ_acc to 200% of Capacity and dP_acc to 200% */ + dq_acc = config->fullcap / dQ_ACC_DIV; + max17042_write_verify_reg(map, MAX17042_dQacc, dq_acc); + max17042_write_verify_reg(map, MAX17042_dPacc, dP_ACC_200); + + max17042_write_verify_reg(map, MAX17042_FullCAP, + config->fullcap); + regmap_write(map, MAX17042_DesignCap, + config->design_cap); + max17042_write_verify_reg(map, MAX17042_FullCAPNom, + config->fullcapnom); + /* Update SOC register with new SOC */ + regmap_write(map, MAX17042_RepSOC, vfSoc); +} + +/* + * Block write all the override values coming from platform data. + * This function MUST be called before the POR initialization proceedure + * specified by maxim. + */ +static inline void max17042_override_por_values(struct max17042_chip *chip) +{ + struct regmap *map = chip->regmap; + struct max17042_config_data *config = chip->pdata->config_data; + + max17042_override_por(map, MAX17042_TGAIN, config->tgain); + max17042_override_por(map, MAX17042_TOFF, config->toff); + max17042_override_por(map, MAX17042_CGAIN, config->cgain); + max17042_override_por(map, MAX17042_COFF, config->coff); + + max17042_override_por(map, MAX17042_VALRT_Th, config->valrt_thresh); + max17042_override_por(map, MAX17042_TALRT_Th, config->talrt_thresh); + max17042_override_por(map, MAX17042_SALRT_Th, + config->soc_alrt_thresh); + max17042_override_por(map, MAX17042_CONFIG, config->config); + max17042_override_por(map, MAX17042_SHDNTIMER, config->shdntimer); + + max17042_override_por(map, MAX17042_DesignCap, config->design_cap); + max17042_override_por(map, MAX17042_ICHGTerm, config->ichgt_term); + + max17042_override_por(map, MAX17042_AtRate, config->at_rate); + max17042_override_por(map, MAX17042_LearnCFG, config->learn_cfg); + max17042_override_por(map, MAX17042_FilterCFG, config->filter_cfg); + max17042_override_por(map, MAX17042_RelaxCFG, config->relax_cfg); + max17042_override_por(map, MAX17042_MiscCFG, config->misc_cfg); + max17042_override_por(map, MAX17042_MaskSOC, config->masksoc); + + max17042_override_por(map, MAX17042_FullCAP, config->fullcap); + max17042_override_por(map, MAX17042_FullCAPNom, config->fullcapnom); + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) + max17042_override_por(map, MAX17042_SOC_empty, + config->socempty); + max17042_override_por(map, MAX17042_LAvg_empty, config->lavg_empty); + max17042_override_por(map, MAX17042_dQacc, config->dqacc); + max17042_override_por(map, MAX17042_dPacc, config->dpacc); + + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) + max17042_override_por(map, MAX17042_V_empty, config->vempty); + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055) + max17042_override_por(map, MAX17055_V_empty, config->vempty); + else + max17042_override_por(map, MAX17047_V_empty, config->vempty); + max17042_override_por(map, MAX17042_TempNom, config->temp_nom); + max17042_override_por(map, MAX17042_TempLim, config->temp_lim); + max17042_override_por(map, MAX17042_FCTC, config->fctc); + max17042_override_por(map, MAX17042_RCOMP0, config->rcomp0); + max17042_override_por(map, MAX17042_TempCo, config->tcompc0); + if (chip->chip_type && + ((chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) || + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047) || + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050))) { + max17042_override_por(map, MAX17042_EmptyTempCo, + config->empty_tempco); + max17042_override_por(map, MAX17042_K_empty0, + config->kempty0); + } +} + +static int max17042_init_chip(struct max17042_chip *chip) +{ + struct regmap *map = chip->regmap; + int ret; + + max17042_override_por_values(chip); + /* After Power up, the MAX17042 requires 500mS in order + * to perform signal debouncing and initial SOC reporting + */ + msleep(500); + + /* Initialize configaration */ + max17042_write_config_regs(chip); + + /* write cell characterization data */ + ret = max17042_init_model(chip); + if (ret) { + dev_err(&chip->client->dev, "%s init failed\n", + __func__); + return -EIO; + } + + ret = max17042_verify_model_lock(chip); + if (ret) { + dev_err(&chip->client->dev, "%s lock verify failed\n", + __func__); + return -EIO; + } + /* write custom parameters */ + max17042_write_custom_regs(chip); + + /* update capacity params */ + max17042_update_capacity_regs(chip); + + /* delay must be atleast 350mS to allow VFSOC + * to be calculated from the new configuration + */ + msleep(350); + + /* reset vfsoc0 reg */ + max17042_reset_vfsoc0_reg(chip); + + /* load new capacity params */ + max17042_load_new_capacity_params(chip); + + /* Init complete, Clear the POR bit */ + regmap_update_bits(map, MAX17042_STATUS, STATUS_POR_BIT, 0x0); + return 0; +} + +static void max17042_set_soc_threshold(struct max17042_chip *chip, u16 off) +{ + struct regmap *map = chip->regmap; + u32 soc, soc_tr; + + /* program interrupt thesholds such that we should + * get interrupt for every 'off' perc change in the soc + */ + regmap_read(map, MAX17042_RepSOC, &soc); + soc >>= 8; + soc_tr = (soc + off) << 8; + if (off < soc) + soc_tr |= soc - off; + regmap_write(map, MAX17042_SALRT_Th, soc_tr); +} + +static irqreturn_t max17042_thread_handler(int id, void *dev) +{ + struct max17042_chip *chip = dev; + u32 val; + int ret; + + ret = regmap_read(chip->regmap, MAX17042_STATUS, &val); + if (ret) + return IRQ_HANDLED; + + if ((val & STATUS_INTR_SOCMIN_BIT) || + (val & STATUS_INTR_SOCMAX_BIT)) { + dev_info(&chip->client->dev, "SOC threshold INTR\n"); + max17042_set_soc_threshold(chip, 1); + } + + /* we implicitly handle all alerts via power_supply_changed */ + regmap_clear_bits(chip->regmap, MAX17042_STATUS, + 0xFFFF & ~(STATUS_POR_BIT | STATUS_BST_BIT)); + + power_supply_changed(chip->battery); + return IRQ_HANDLED; +} + +static void max17042_init_worker(struct work_struct *work) +{ + struct max17042_chip *chip = container_of(work, + struct max17042_chip, work); + int ret; + + /* Initialize registers according to values from the platform data */ + if (chip->pdata->enable_por_init && chip->pdata->config_data) { + ret = max17042_init_chip(chip); + if (ret) + return; + } + + chip->init_complete = 1; +} + +#ifdef CONFIG_OF +static struct max17042_platform_data * +max17042_get_of_pdata(struct max17042_chip *chip) +{ + struct device *dev = &chip->client->dev; + struct device_node *np = dev->of_node; + u32 prop; + struct max17042_platform_data *pdata; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return NULL; + + /* + * Require current sense resistor value to be specified for + * current-sense functionality to be enabled at all. + */ + if (of_property_read_u32(np, "maxim,rsns-microohm", &prop) == 0) { + pdata->r_sns = prop; + pdata->enable_current_sense = true; + } + + if (of_property_read_s32(np, "maxim,cold-temp", &pdata->temp_min)) + pdata->temp_min = INT_MIN; + if (of_property_read_s32(np, "maxim,over-heat-temp", &pdata->temp_max)) + pdata->temp_max = INT_MAX; + if (of_property_read_s32(np, "maxim,dead-volt", &pdata->vmin)) + pdata->vmin = INT_MIN; + if (of_property_read_s32(np, "maxim,over-volt", &pdata->vmax)) + pdata->vmax = INT_MAX; + + return pdata; +} +#endif + +static struct max17042_reg_data max17047_default_pdata_init_regs[] = { + /* + * Some firmwares do not set FullSOCThr, Enable End-of-Charge Detection + * when the voltage FG reports 95%, as recommended in the datasheet. + */ + { MAX17047_FullSOCThr, MAX17042_BATTERY_FULL << 8 }, +}; + +static struct max17042_platform_data * +max17042_get_default_pdata(struct max17042_chip *chip) +{ + struct device *dev = &chip->client->dev; + struct max17042_platform_data *pdata; + int ret, misc_cfg; + + /* + * The MAX17047 gets used on x86 where we might not have pdata, assume + * the firmware will already have initialized the fuel-gauge and provide + * default values for the non init bits to make things work. + */ + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return pdata; + + if ((chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047) || + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050)) { + pdata->init_data = max17047_default_pdata_init_regs; + pdata->num_init_data = + ARRAY_SIZE(max17047_default_pdata_init_regs); + } + + ret = regmap_read(chip->regmap, MAX17042_MiscCFG, &misc_cfg); + if (ret < 0) + return NULL; + + /* If bits 0-1 are set to 3 then only Voltage readings are used */ + if ((misc_cfg & 0x3) == 0x3) + pdata->enable_current_sense = false; + else + pdata->enable_current_sense = true; + + pdata->vmin = MAX17042_DEFAULT_VMIN; + pdata->vmax = MAX17042_DEFAULT_VMAX; + pdata->temp_min = MAX17042_DEFAULT_TEMP_MIN; + pdata->temp_max = MAX17042_DEFAULT_TEMP_MAX; + + return pdata; +} + +static struct max17042_platform_data * +max17042_get_pdata(struct max17042_chip *chip) +{ + struct device *dev = &chip->client->dev; + +#ifdef CONFIG_OF + if (dev->of_node) + return max17042_get_of_pdata(chip); +#endif + if (dev->platform_data) + return dev->platform_data; + + return max17042_get_default_pdata(chip); +} + +static const struct regmap_config max17042_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + .val_format_endian = REGMAP_ENDIAN_NATIVE, +}; + +static const struct power_supply_desc max17042_psy_desc = { + .name = "max170xx_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = max17042_get_property, + .set_property = max17042_set_property, + .property_is_writeable = max17042_property_is_writeable, + .external_power_changed = max17042_external_power_changed, + .properties = max17042_battery_props, + .num_properties = ARRAY_SIZE(max17042_battery_props), +}; + +static const struct power_supply_desc max17042_no_current_sense_psy_desc = { + .name = "max170xx_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = max17042_get_property, + .set_property = max17042_set_property, + .property_is_writeable = max17042_property_is_writeable, + .properties = max17042_battery_props, + .num_properties = ARRAY_SIZE(max17042_battery_props) - 2, +}; + +static void max17042_stop_work(void *data) +{ + struct max17042_chip *chip = data; + + cancel_work_sync(&chip->work); +} + +static int max17042_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + const struct power_supply_desc *max17042_desc = &max17042_psy_desc; + struct power_supply_config psy_cfg = {}; + const struct acpi_device_id *acpi_id = NULL; + struct device *dev = &client->dev; + struct max17042_chip *chip; + int ret; + int i; + u32 val; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_WORD_DATA)) + return -EIO; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->client = client; + if (id) { + chip->chip_type = id->driver_data; + } else { + acpi_id = acpi_match_device(dev->driver->acpi_match_table, dev); + if (!acpi_id) + return -ENODEV; + + chip->chip_type = acpi_id->driver_data; + } + chip->regmap = devm_regmap_init_i2c(client, &max17042_regmap_config); + if (IS_ERR(chip->regmap)) { + dev_err(&client->dev, "Failed to initialize regmap\n"); + return -EINVAL; + } + + chip->pdata = max17042_get_pdata(chip); + if (!chip->pdata) { + dev_err(&client->dev, "no platform data provided\n"); + return -EINVAL; + } + + i2c_set_clientdata(client, chip); + psy_cfg.drv_data = chip; + psy_cfg.of_node = dev->of_node; + + /* When current is not measured, + * CURRENT_NOW and CURRENT_AVG properties should be invisible. */ + if (!chip->pdata->enable_current_sense) + max17042_desc = &max17042_no_current_sense_psy_desc; + + if (chip->pdata->r_sns == 0) + chip->pdata->r_sns = MAX17042_DEFAULT_SNS_RESISTOR; + + if (chip->pdata->init_data) + for (i = 0; i < chip->pdata->num_init_data; i++) + regmap_write(chip->regmap, + chip->pdata->init_data[i].addr, + chip->pdata->init_data[i].data); + + if (!chip->pdata->enable_current_sense) { + regmap_write(chip->regmap, MAX17042_CGAIN, 0x0000); + regmap_write(chip->regmap, MAX17042_MiscCFG, 0x0003); + regmap_write(chip->regmap, MAX17042_LearnCFG, 0x0007); + } + + chip->battery = devm_power_supply_register(&client->dev, max17042_desc, + &psy_cfg); + if (IS_ERR(chip->battery)) { + dev_err(&client->dev, "failed: power supply register\n"); + return PTR_ERR(chip->battery); + } + + if (client->irq) { + unsigned int flags = IRQF_ONESHOT; + + /* + * On ACPI systems the IRQ may be handled by ACPI-event code, + * so we need to share (if the ACPI code is willing to share). + */ + if (acpi_id) + flags |= IRQF_SHARED | IRQF_PROBE_SHARED; + + ret = devm_request_threaded_irq(&client->dev, client->irq, + NULL, + max17042_thread_handler, flags, + chip->battery->desc->name, + chip); + if (!ret) { + regmap_update_bits(chip->regmap, MAX17042_CONFIG, + CONFIG_ALRT_BIT_ENBL, + CONFIG_ALRT_BIT_ENBL); + max17042_set_soc_threshold(chip, 1); + } else { + client->irq = 0; + if (ret != -EBUSY) + dev_err(&client->dev, "Failed to get IRQ\n"); + } + } + /* Not able to update the charge threshold when exceeded? -> disable */ + if (!client->irq) + regmap_write(chip->regmap, MAX17042_SALRT_Th, 0xff00); + + regmap_read(chip->regmap, MAX17042_STATUS, &val); + if (val & STATUS_POR_BIT) { + INIT_WORK(&chip->work, max17042_init_worker); + ret = devm_add_action(&client->dev, max17042_stop_work, chip); + if (ret) + return ret; + schedule_work(&chip->work); + } else { + chip->init_complete = 1; + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int max17042_suspend(struct device *dev) +{ + struct max17042_chip *chip = dev_get_drvdata(dev); + + /* + * disable the irq and enable irq_wake + * capability to the interrupt line. + */ + if (chip->client->irq) { + disable_irq(chip->client->irq); + enable_irq_wake(chip->client->irq); + } + + return 0; +} + +static int max17042_resume(struct device *dev) +{ + struct max17042_chip *chip = dev_get_drvdata(dev); + + if (chip->client->irq) { + disable_irq_wake(chip->client->irq); + enable_irq(chip->client->irq); + /* re-program the SOC thresholds to 1% change */ + max17042_set_soc_threshold(chip, 1); + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(max17042_pm_ops, max17042_suspend, + max17042_resume); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id max17042_acpi_match[] = { + { "MAX17047", MAXIM_DEVICE_TYPE_MAX17047 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, max17042_acpi_match); +#endif + +#ifdef CONFIG_OF +static const struct of_device_id max17042_dt_match[] = { + { .compatible = "maxim,max17042" }, + { .compatible = "maxim,max17047" }, + { .compatible = "maxim,max17050" }, + { .compatible = "maxim,max17055" }, + { }, +}; +MODULE_DEVICE_TABLE(of, max17042_dt_match); +#endif + +static const struct i2c_device_id max17042_id[] = { + { "max17042", MAXIM_DEVICE_TYPE_MAX17042 }, + { "max17047", MAXIM_DEVICE_TYPE_MAX17047 }, + { "max17050", MAXIM_DEVICE_TYPE_MAX17050 }, + { "max17055", MAXIM_DEVICE_TYPE_MAX17055 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max17042_id); + +static struct i2c_driver max17042_i2c_driver = { + .driver = { + .name = "max17042", + .acpi_match_table = ACPI_PTR(max17042_acpi_match), + .of_match_table = of_match_ptr(max17042_dt_match), + .pm = &max17042_pm_ops, + }, + .probe = max17042_probe, + .id_table = max17042_id, +}; +module_i2c_driver(max17042_i2c_driver); + +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_DESCRIPTION("MAX17042 Fuel Gauge"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max1721x_battery.c b/drivers/power/supply/max1721x_battery.c new file mode 100644 index 000000000..1b1a36f8e --- /dev/null +++ b/drivers/power/supply/max1721x_battery.c @@ -0,0 +1,448 @@ +/* + * 1-Wire implementation for Maxim Semiconductor + * MAX7211/MAX17215 stanalone fuel gauge chip + * + * Copyright (C) 2017 Radioavionica Corporation + * Author: Alex A. Mihaylov <minimumlaw@rambler.ru> + * + * Use consistent with the GNU GPL is permitted, + * provided that this copyright notice is + * preserved in its entirety in all copies and derived works. + * + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/w1.h> +#include <linux/regmap.h> +#include <linux/power_supply.h> + +#define W1_MAX1721X_FAMILY_ID 0x26 +#define DEF_DEV_NAME_MAX17211 "MAX17211" +#define DEF_DEV_NAME_MAX17215 "MAX17215" +#define DEF_DEV_NAME_UNKNOWN "UNKNOWN" +#define DEF_MFG_NAME "MAXIM" + +#define PSY_MAX_NAME_LEN 32 + +/* Number of valid register addresses in W1 mode */ +#define MAX1721X_MAX_REG_NR 0x1EF + +/* Factory settings (nonvilatile registers) (W1 specific) */ +#define MAX1721X_REG_NRSENSE 0x1CF /* RSense in 10^-5 Ohm */ +/* Strings */ +#define MAX1721X_REG_MFG_STR 0x1CC +#define MAX1721X_REG_MFG_NUMB 3 +#define MAX1721X_REG_DEV_STR 0x1DB +#define MAX1721X_REG_DEV_NUMB 5 +/* HEX Strings */ +#define MAX1721X_REG_SER_HEX 0x1D8 + +/* MAX172XX Output Registers for W1 chips */ +#define MAX172XX_REG_STATUS 0x000 /* status reg */ +#define MAX172XX_BAT_PRESENT (1<<4) /* battery connected bit */ +#define MAX172XX_REG_DEVNAME 0x021 /* chip config */ +#define MAX172XX_DEV_MASK 0x000F /* chip type mask */ +#define MAX172X1_DEV 0x0001 +#define MAX172X5_DEV 0x0005 +#define MAX172XX_REG_TEMP 0x008 /* Temperature */ +#define MAX172XX_REG_BATT 0x0DA /* Battery voltage */ +#define MAX172XX_REG_CURRENT 0x00A /* Actual current */ +#define MAX172XX_REG_AVGCURRENT 0x00B /* Average current */ +#define MAX172XX_REG_REPSOC 0x006 /* Percentage of charge */ +#define MAX172XX_REG_DESIGNCAP 0x018 /* Design capacity */ +#define MAX172XX_REG_REPCAP 0x005 /* Average capacity */ +#define MAX172XX_REG_TTE 0x011 /* Time to empty */ +#define MAX172XX_REG_TTF 0x020 /* Time to full */ + +struct max17211_device_info { + char name[PSY_MAX_NAME_LEN]; + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct device *w1_dev; + struct regmap *regmap; + /* battery design format */ + unsigned int rsense; /* in tenths uOhm */ + char DeviceName[2 * MAX1721X_REG_DEV_NUMB + 1]; + char ManufacturerName[2 * MAX1721X_REG_MFG_NUMB + 1]; + char SerialNumber[13]; /* see get_sn_str() later for comment */ +}; + +/* Convert regs value to power_supply units */ + +static inline int max172xx_time_to_ps(unsigned int reg) +{ + return reg * 5625 / 1000; /* in sec. */ +} + +static inline int max172xx_percent_to_ps(unsigned int reg) +{ + return reg / 256; /* in percent from 0 to 100 */ +} + +static inline int max172xx_voltage_to_ps(unsigned int reg) +{ + return reg * 1250; /* in uV */ +} + +static inline int max172xx_capacity_to_ps(unsigned int reg) +{ + return reg * 500; /* in uAh */ +} + +/* + * Current and temperature is signed values, so unsigned regs + * value must be converted to signed type + */ + +static inline int max172xx_temperature_to_ps(unsigned int reg) +{ + int val = (int16_t)(reg); + + return val * 10 / 256; /* in tenths of deg. C */ +} + +/* + * Calculating current registers resolution: + * + * RSense stored in 10^-5 Ohm, so mesaurment voltage must be + * in 10^-11 Volts for get current in uA. + * 16 bit current reg fullscale +/-51.2mV is 102400 uV. + * So: 102400 / 65535 * 10^5 = 156252 + */ +static inline int max172xx_current_to_voltage(unsigned int reg) +{ + int val = (int16_t)(reg); + + return val * 156252; +} + + +static inline struct max17211_device_info * +to_device_info(struct power_supply *psy) +{ + return power_supply_get_drvdata(psy); +} + +static int max1721x_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max17211_device_info *info = to_device_info(psy); + unsigned int reg = 0; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + /* + * POWER_SUPPLY_PROP_PRESENT will always readable via + * sysfs interface. Value return 0 if battery not + * present or unaccesable via W1. + */ + val->intval = + regmap_read(info->regmap, MAX172XX_REG_STATUS, + ®) ? 0 : !(reg & MAX172XX_BAT_PRESENT); + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = regmap_read(info->regmap, MAX172XX_REG_REPSOC, ®); + val->intval = max172xx_percent_to_ps(reg); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = regmap_read(info->regmap, MAX172XX_REG_BATT, ®); + val->intval = max172xx_voltage_to_ps(reg); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = regmap_read(info->regmap, MAX172XX_REG_DESIGNCAP, ®); + val->intval = max172xx_capacity_to_ps(reg); + break; + case POWER_SUPPLY_PROP_CHARGE_AVG: + ret = regmap_read(info->regmap, MAX172XX_REG_REPCAP, ®); + val->intval = max172xx_capacity_to_ps(reg); + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + ret = regmap_read(info->regmap, MAX172XX_REG_TTE, ®); + val->intval = max172xx_time_to_ps(reg); + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_AVG: + ret = regmap_read(info->regmap, MAX172XX_REG_TTF, ®); + val->intval = max172xx_time_to_ps(reg); + break; + case POWER_SUPPLY_PROP_TEMP: + ret = regmap_read(info->regmap, MAX172XX_REG_TEMP, ®); + val->intval = max172xx_temperature_to_ps(reg); + break; + /* We need signed current, so must cast info->rsense to signed type */ + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = regmap_read(info->regmap, MAX172XX_REG_CURRENT, ®); + val->intval = + max172xx_current_to_voltage(reg) / (int)info->rsense; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = regmap_read(info->regmap, MAX172XX_REG_AVGCURRENT, ®); + val->intval = + max172xx_current_to_voltage(reg) / (int)info->rsense; + break; + /* + * Strings already received and inited by probe. + * We do dummy read for check battery still available. + */ + case POWER_SUPPLY_PROP_MODEL_NAME: + ret = regmap_read(info->regmap, MAX1721X_REG_DEV_STR, ®); + val->strval = info->DeviceName; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + ret = regmap_read(info->regmap, MAX1721X_REG_MFG_STR, ®); + val->strval = info->ManufacturerName; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + ret = regmap_read(info->regmap, MAX1721X_REG_SER_HEX, ®); + val->strval = info->SerialNumber; + break; + default: + ret = -EINVAL; + } + + return ret; +} + +static enum power_supply_property max1721x_battery_props[] = { + /* int */ + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_AVG, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_AVG, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + /* strings */ + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, +}; + +static int get_string(struct max17211_device_info *info, + uint16_t reg, uint8_t nr, char *str) +{ + unsigned int val; + + if (!str || !(reg == MAX1721X_REG_MFG_STR || + reg == MAX1721X_REG_DEV_STR)) + return -EFAULT; + + while (nr--) { + if (regmap_read(info->regmap, reg++, &val)) + return -EFAULT; + *str++ = val>>8 & 0x00FF; + *str++ = val & 0x00FF; + } + return 0; +} + +/* Maxim say: Serial number is a hex string up to 12 hex characters */ +static int get_sn_string(struct max17211_device_info *info, char *str) +{ + unsigned int val[3]; + + if (!str) + return -EFAULT; + + if (regmap_read(info->regmap, MAX1721X_REG_SER_HEX, &val[0])) + return -EFAULT; + if (regmap_read(info->regmap, MAX1721X_REG_SER_HEX + 1, &val[1])) + return -EFAULT; + if (regmap_read(info->regmap, MAX1721X_REG_SER_HEX + 2, &val[2])) + return -EFAULT; + + snprintf(str, 13, "%04X%04X%04X", val[0], val[1], val[2]); + return 0; +} + +/* + * MAX1721x registers description for w1-regmap + */ +static const struct regmap_range max1721x_allow_range[] = { + regmap_reg_range(0, 0xDF), /* volatile data */ + regmap_reg_range(0x180, 0x1DF), /* non-volatile memory */ + regmap_reg_range(0x1E0, 0x1EF), /* non-volatile history (unused) */ +}; + +static const struct regmap_range max1721x_deny_range[] = { + /* volatile data unused registers */ + regmap_reg_range(0x24, 0x26), + regmap_reg_range(0x30, 0x31), + regmap_reg_range(0x33, 0x34), + regmap_reg_range(0x37, 0x37), + regmap_reg_range(0x3B, 0x3C), + regmap_reg_range(0x40, 0x41), + regmap_reg_range(0x43, 0x44), + regmap_reg_range(0x47, 0x49), + regmap_reg_range(0x4B, 0x4C), + regmap_reg_range(0x4E, 0xAF), + regmap_reg_range(0xB1, 0xB3), + regmap_reg_range(0xB5, 0xB7), + regmap_reg_range(0xBF, 0xD0), + regmap_reg_range(0xDB, 0xDB), + /* hole between volatile and non-volatile registers */ + regmap_reg_range(0xE0, 0x17F), +}; + +static const struct regmap_access_table max1721x_regs = { + .yes_ranges = max1721x_allow_range, + .n_yes_ranges = ARRAY_SIZE(max1721x_allow_range), + .no_ranges = max1721x_deny_range, + .n_no_ranges = ARRAY_SIZE(max1721x_deny_range), +}; + +/* + * Model Gauge M5 Algorithm output register + * Volatile data (must not be cached) + */ +static const struct regmap_range max1721x_volatile_allow[] = { + regmap_reg_range(0, 0xDF), +}; + +static const struct regmap_access_table max1721x_volatile_regs = { + .yes_ranges = max1721x_volatile_allow, + .n_yes_ranges = ARRAY_SIZE(max1721x_volatile_allow), +}; + +/* + * W1-regmap config + */ +static const struct regmap_config max1721x_regmap_w1_config = { + .reg_bits = 16, + .val_bits = 16, + .rd_table = &max1721x_regs, + .volatile_table = &max1721x_volatile_regs, + .max_register = MAX1721X_MAX_REG_NR, +}; + +static int devm_w1_max1721x_add_device(struct w1_slave *sl) +{ + struct power_supply_config psy_cfg = {}; + struct max17211_device_info *info; + + info = devm_kzalloc(&sl->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + sl->family_data = (void *)info; + info->w1_dev = &sl->dev; + + /* + * power_supply class battery name translated from W1 slave device + * unical ID (look like 26-0123456789AB) to "max1721x-0123456789AB\0" + * so, 26 (device family) correcpondent to max1721x devices. + * Device name still unical for any numbers connected devices. + */ + snprintf(info->name, sizeof(info->name), + "max1721x-%012X", (unsigned int)sl->reg_num.id); + info->bat_desc.name = info->name; + + /* + * FixMe: battery device name exceed max len for thermal_zone device + * name and translation to thermal_zone must be disabled. + */ + info->bat_desc.no_thermal = true; + info->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY; + info->bat_desc.properties = max1721x_battery_props; + info->bat_desc.num_properties = ARRAY_SIZE(max1721x_battery_props); + info->bat_desc.get_property = max1721x_battery_get_property; + psy_cfg.drv_data = info; + + /* regmap init */ + info->regmap = devm_regmap_init_w1(info->w1_dev, + &max1721x_regmap_w1_config); + if (IS_ERR(info->regmap)) { + int err = PTR_ERR(info->regmap); + + dev_err(info->w1_dev, "Failed to allocate register map: %d\n", + err); + return err; + } + + /* rsense init */ + info->rsense = 0; + if (regmap_read(info->regmap, MAX1721X_REG_NRSENSE, &info->rsense)) { + dev_err(info->w1_dev, "Can't read RSense. Hardware error.\n"); + return -ENODEV; + } + + if (!info->rsense) { + dev_warn(info->w1_dev, "RSense not calibrated, set 10 mOhms!\n"); + info->rsense = 1000; /* in regs in 10^-5 */ + } + dev_info(info->w1_dev, "RSense: %d mOhms.\n", info->rsense / 100); + + if (get_string(info, MAX1721X_REG_MFG_STR, + MAX1721X_REG_MFG_NUMB, info->ManufacturerName)) { + dev_err(info->w1_dev, "Can't read manufacturer. Hardware error.\n"); + return -ENODEV; + } + + if (!info->ManufacturerName[0]) + strncpy(info->ManufacturerName, DEF_MFG_NAME, + 2 * MAX1721X_REG_MFG_NUMB); + + if (get_string(info, MAX1721X_REG_DEV_STR, + MAX1721X_REG_DEV_NUMB, info->DeviceName)) { + dev_err(info->w1_dev, "Can't read device. Hardware error.\n"); + return -ENODEV; + } + if (!info->DeviceName[0]) { + unsigned int dev_name; + + if (regmap_read(info->regmap, + MAX172XX_REG_DEVNAME, &dev_name)) { + dev_err(info->w1_dev, "Can't read device name reg.\n"); + return -ENODEV; + } + + switch (dev_name & MAX172XX_DEV_MASK) { + case MAX172X1_DEV: + strncpy(info->DeviceName, DEF_DEV_NAME_MAX17211, + 2 * MAX1721X_REG_DEV_NUMB); + break; + case MAX172X5_DEV: + strncpy(info->DeviceName, DEF_DEV_NAME_MAX17215, + 2 * MAX1721X_REG_DEV_NUMB); + break; + default: + strncpy(info->DeviceName, DEF_DEV_NAME_UNKNOWN, + 2 * MAX1721X_REG_DEV_NUMB); + } + } + + if (get_sn_string(info, info->SerialNumber)) { + dev_err(info->w1_dev, "Can't read serial. Hardware error.\n"); + return -ENODEV; + } + + info->bat = devm_power_supply_register(&sl->dev, &info->bat_desc, + &psy_cfg); + if (IS_ERR(info->bat)) { + dev_err(info->w1_dev, "failed to register battery\n"); + return PTR_ERR(info->bat); + } + + return 0; +} + +static const struct w1_family_ops w1_max1721x_fops = { + .add_slave = devm_w1_max1721x_add_device, +}; + +static struct w1_family w1_max1721x_family = { + .fid = W1_MAX1721X_FAMILY_ID, + .fops = &w1_max1721x_fops, +}; + +module_w1_family(w1_max1721x_family); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Alex A. Mihaylov <minimumlaw@rambler.ru>"); +MODULE_DESCRIPTION("Maxim MAX17211/MAX17215 Fuel Gauage IC driver"); +MODULE_ALIAS("w1-family-" __stringify(W1_MAX1721X_FAMILY_ID)); diff --git a/drivers/power/supply/max77650-charger.c b/drivers/power/supply/max77650-charger.c new file mode 100644 index 000000000..d913428be --- /dev/null +++ b/drivers/power/supply/max77650-charger.c @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright (C) 2018 BayLibre SAS +// Author: Bartosz Golaszewski <bgolaszewski@baylibre.com> +// +// Battery charger driver for MAXIM 77650/77651 charger/power-supply. + +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/mfd/max77650.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> + +#define MAX77650_CHARGER_ENABLED BIT(0) +#define MAX77650_CHARGER_DISABLED 0x00 +#define MAX77650_CHARGER_CHG_EN_MASK BIT(0) + +#define MAX77650_CHG_DETAILS_MASK GENMASK(7, 4) +#define MAX77650_CHG_DETAILS_BITS(_reg) \ + (((_reg) & MAX77650_CHG_DETAILS_MASK) >> 4) + +/* Charger is OFF. */ +#define MAX77650_CHG_OFF 0x00 +/* Charger is in prequalification mode. */ +#define MAX77650_CHG_PREQ 0x01 +/* Charger is in fast-charge constant current mode. */ +#define MAX77650_CHG_ON_CURR 0x02 +/* Charger is in JEITA modified fast-charge constant-current mode. */ +#define MAX77650_CHG_ON_CURR_JEITA 0x03 +/* Charger is in fast-charge constant-voltage mode. */ +#define MAX77650_CHG_ON_VOLT 0x04 +/* Charger is in JEITA modified fast-charge constant-voltage mode. */ +#define MAX77650_CHG_ON_VOLT_JEITA 0x05 +/* Charger is in top-off mode. */ +#define MAX77650_CHG_ON_TOPOFF 0x06 +/* Charger is in JEITA modified top-off mode. */ +#define MAX77650_CHG_ON_TOPOFF_JEITA 0x07 +/* Charger is done. */ +#define MAX77650_CHG_DONE 0x08 +/* Charger is JEITA modified done. */ +#define MAX77650_CHG_DONE_JEITA 0x09 +/* Charger is suspended due to a prequalification timer fault. */ +#define MAX77650_CHG_SUSP_PREQ_TIM_FAULT 0x0a +/* Charger is suspended due to a fast-charge timer fault. */ +#define MAX77650_CHG_SUSP_FAST_CHG_TIM_FAULT 0x0b +/* Charger is suspended due to a battery temperature fault. */ +#define MAX77650_CHG_SUSP_BATT_TEMP_FAULT 0x0c + +#define MAX77650_CHGIN_DETAILS_MASK GENMASK(3, 2) +#define MAX77650_CHGIN_DETAILS_BITS(_reg) \ + (((_reg) & MAX77650_CHGIN_DETAILS_MASK) >> 2) + +#define MAX77650_CHGIN_UNDERVOLTAGE_LOCKOUT 0x00 +#define MAX77650_CHGIN_OVERVOLTAGE_LOCKOUT 0x01 +#define MAX77650_CHGIN_OKAY 0x11 + +#define MAX77650_CHARGER_CHG_MASK BIT(1) +#define MAX77650_CHARGER_CHG_CHARGING(_reg) \ + (((_reg) & MAX77650_CHARGER_CHG_MASK) > 1) + +#define MAX77650_CHARGER_VCHGIN_MIN_MASK 0xc0 +#define MAX77650_CHARGER_VCHGIN_MIN_SHIFT(_val) ((_val) << 5) + +#define MAX77650_CHARGER_ICHGIN_LIM_MASK 0x1c +#define MAX77650_CHARGER_ICHGIN_LIM_SHIFT(_val) ((_val) << 2) + +struct max77650_charger_data { + struct regmap *map; + struct device *dev; +}; + +static enum power_supply_property max77650_charger_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CHARGE_TYPE +}; + +static const unsigned int max77650_charger_vchgin_min_table[] = { + 4000000, 4100000, 4200000, 4300000, 4400000, 4500000, 4600000, 4700000 +}; + +static const unsigned int max77650_charger_ichgin_lim_table[] = { + 95000, 190000, 285000, 380000, 475000 +}; + +static int max77650_charger_set_vchgin_min(struct max77650_charger_data *chg, + unsigned int val) +{ + int i, rv; + + for (i = 0; i < ARRAY_SIZE(max77650_charger_vchgin_min_table); i++) { + if (val == max77650_charger_vchgin_min_table[i]) { + rv = regmap_update_bits(chg->map, + MAX77650_REG_CNFG_CHG_B, + MAX77650_CHARGER_VCHGIN_MIN_MASK, + MAX77650_CHARGER_VCHGIN_MIN_SHIFT(i)); + if (rv) + return rv; + + return 0; + } + } + + return -EINVAL; +} + +static int max77650_charger_set_ichgin_lim(struct max77650_charger_data *chg, + unsigned int val) +{ + int i, rv; + + for (i = 0; i < ARRAY_SIZE(max77650_charger_ichgin_lim_table); i++) { + if (val == max77650_charger_ichgin_lim_table[i]) { + rv = regmap_update_bits(chg->map, + MAX77650_REG_CNFG_CHG_B, + MAX77650_CHARGER_ICHGIN_LIM_MASK, + MAX77650_CHARGER_ICHGIN_LIM_SHIFT(i)); + if (rv) + return rv; + + return 0; + } + } + + return -EINVAL; +} + +static int max77650_charger_enable(struct max77650_charger_data *chg) +{ + int rv; + + rv = regmap_update_bits(chg->map, + MAX77650_REG_CNFG_CHG_B, + MAX77650_CHARGER_CHG_EN_MASK, + MAX77650_CHARGER_ENABLED); + if (rv) + dev_err(chg->dev, "unable to enable the charger: %d\n", rv); + + return rv; +} + +static int max77650_charger_disable(struct max77650_charger_data *chg) +{ + int rv; + + rv = regmap_update_bits(chg->map, + MAX77650_REG_CNFG_CHG_B, + MAX77650_CHARGER_CHG_EN_MASK, + MAX77650_CHARGER_DISABLED); + if (rv) + dev_err(chg->dev, "unable to disable the charger: %d\n", rv); + + return rv; +} + +static irqreturn_t max77650_charger_check_status(int irq, void *data) +{ + struct max77650_charger_data *chg = data; + int rv, reg; + + rv = regmap_read(chg->map, MAX77650_REG_STAT_CHG_B, ®); + if (rv) { + dev_err(chg->dev, + "unable to read the charger status: %d\n", rv); + return IRQ_HANDLED; + } + + switch (MAX77650_CHGIN_DETAILS_BITS(reg)) { + case MAX77650_CHGIN_UNDERVOLTAGE_LOCKOUT: + dev_err(chg->dev, "undervoltage lockout detected, disabling charger\n"); + max77650_charger_disable(chg); + break; + case MAX77650_CHGIN_OVERVOLTAGE_LOCKOUT: + dev_err(chg->dev, "overvoltage lockout detected, disabling charger\n"); + max77650_charger_disable(chg); + break; + case MAX77650_CHGIN_OKAY: + max77650_charger_enable(chg); + break; + default: + /* May be 0x10 - debouncing */ + break; + } + + return IRQ_HANDLED; +} + +static int max77650_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max77650_charger_data *chg = power_supply_get_drvdata(psy); + int rv, reg; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + rv = regmap_read(chg->map, MAX77650_REG_STAT_CHG_B, ®); + if (rv) + return rv; + + if (MAX77650_CHARGER_CHG_CHARGING(reg)) { + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + } + + switch (MAX77650_CHG_DETAILS_BITS(reg)) { + case MAX77650_CHG_OFF: + case MAX77650_CHG_SUSP_PREQ_TIM_FAULT: + case MAX77650_CHG_SUSP_FAST_CHG_TIM_FAULT: + case MAX77650_CHG_SUSP_BATT_TEMP_FAULT: + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case MAX77650_CHG_PREQ: + case MAX77650_CHG_ON_CURR: + case MAX77650_CHG_ON_CURR_JEITA: + case MAX77650_CHG_ON_VOLT: + case MAX77650_CHG_ON_VOLT_JEITA: + case MAX77650_CHG_ON_TOPOFF: + case MAX77650_CHG_ON_TOPOFF_JEITA: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case MAX77650_CHG_DONE: + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + } + break; + case POWER_SUPPLY_PROP_ONLINE: + rv = regmap_read(chg->map, MAX77650_REG_STAT_CHG_B, ®); + if (rv) + return rv; + + val->intval = MAX77650_CHARGER_CHG_CHARGING(reg); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + rv = regmap_read(chg->map, MAX77650_REG_STAT_CHG_B, ®); + if (rv) + return rv; + + if (!MAX77650_CHARGER_CHG_CHARGING(reg)) { + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + } + + switch (MAX77650_CHG_DETAILS_BITS(reg)) { + case MAX77650_CHG_PREQ: + case MAX77650_CHG_ON_CURR: + case MAX77650_CHG_ON_CURR_JEITA: + case MAX77650_CHG_ON_VOLT: + case MAX77650_CHG_ON_VOLT_JEITA: + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case MAX77650_CHG_ON_TOPOFF: + case MAX77650_CHG_ON_TOPOFF_JEITA: + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + default: + val->intval = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct power_supply_desc max77650_battery_desc = { + .name = "max77650", + .type = POWER_SUPPLY_TYPE_USB, + .get_property = max77650_charger_get_property, + .properties = max77650_charger_properties, + .num_properties = ARRAY_SIZE(max77650_charger_properties), +}; + +static int max77650_charger_probe(struct platform_device *pdev) +{ + struct power_supply_config pscfg = {}; + struct max77650_charger_data *chg; + struct power_supply *battery; + struct device *dev, *parent; + int rv, chg_irq, chgin_irq; + unsigned int prop; + + dev = &pdev->dev; + parent = dev->parent; + + chg = devm_kzalloc(dev, sizeof(*chg), GFP_KERNEL); + if (!chg) + return -ENOMEM; + + platform_set_drvdata(pdev, chg); + + chg->map = dev_get_regmap(parent, NULL); + if (!chg->map) + return -ENODEV; + + chg->dev = dev; + + pscfg.of_node = dev->of_node; + pscfg.drv_data = chg; + + chg_irq = platform_get_irq_byname(pdev, "CHG"); + if (chg_irq < 0) + return chg_irq; + + chgin_irq = platform_get_irq_byname(pdev, "CHGIN"); + if (chgin_irq < 0) + return chgin_irq; + + rv = devm_request_any_context_irq(dev, chg_irq, + max77650_charger_check_status, + IRQF_ONESHOT, "chg", chg); + if (rv < 0) + return rv; + + rv = devm_request_any_context_irq(dev, chgin_irq, + max77650_charger_check_status, + IRQF_ONESHOT, "chgin", chg); + if (rv < 0) + return rv; + + battery = devm_power_supply_register(dev, + &max77650_battery_desc, &pscfg); + if (IS_ERR(battery)) + return PTR_ERR(battery); + + rv = of_property_read_u32(dev->of_node, + "input-voltage-min-microvolt", &prop); + if (rv == 0) { + rv = max77650_charger_set_vchgin_min(chg, prop); + if (rv) + return rv; + } + + rv = of_property_read_u32(dev->of_node, + "input-current-limit-microamp", &prop); + if (rv == 0) { + rv = max77650_charger_set_ichgin_lim(chg, prop); + if (rv) + return rv; + } + + return max77650_charger_enable(chg); +} + +static int max77650_charger_remove(struct platform_device *pdev) +{ + struct max77650_charger_data *chg = platform_get_drvdata(pdev); + + return max77650_charger_disable(chg); +} + +static const struct of_device_id max77650_charger_of_match[] = { + { .compatible = "maxim,max77650-charger" }, + { } +}; +MODULE_DEVICE_TABLE(of, max77650_charger_of_match); + +static struct platform_driver max77650_charger_driver = { + .driver = { + .name = "max77650-charger", + .of_match_table = max77650_charger_of_match, + }, + .probe = max77650_charger_probe, + .remove = max77650_charger_remove, +}; +module_platform_driver(max77650_charger_driver); + +MODULE_DESCRIPTION("MAXIM 77650/77651 charger driver"); +MODULE_AUTHOR("Bartosz Golaszewski <bgolaszewski@baylibre.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:max77650-charger"); diff --git a/drivers/power/supply/max77693_charger.c b/drivers/power/supply/max77693_charger.c new file mode 100644 index 000000000..a2c5c9858 --- /dev/null +++ b/drivers/power/supply/max77693_charger.c @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// max77693_charger.c - Battery charger driver for the Maxim 77693 +// +// Copyright (C) 2014 Samsung Electronics +// Krzysztof Kozlowski <krzk@kernel.org> + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/mfd/max77693.h> +#include <linux/mfd/max77693-common.h> +#include <linux/mfd/max77693-private.h> + +#define MAX77693_CHARGER_NAME "max77693-charger" +static const char *max77693_charger_model = "MAX77693"; +static const char *max77693_charger_manufacturer = "Maxim Integrated"; + +struct max77693_charger { + struct device *dev; + struct max77693_dev *max77693; + struct power_supply *charger; + + u32 constant_volt; + u32 min_system_volt; + u32 thermal_regulation_temp; + u32 batttery_overcurrent; + u32 charge_input_threshold_volt; +}; + +static int max77693_get_charger_state(struct regmap *regmap, int *val) +{ + int ret; + unsigned int data; + + ret = regmap_read(regmap, MAX77693_CHG_REG_CHG_DETAILS_01, &data); + if (ret < 0) + return ret; + + data &= CHG_DETAILS_01_CHG_MASK; + data >>= CHG_DETAILS_01_CHG_SHIFT; + + switch (data) { + case MAX77693_CHARGING_PREQUALIFICATION: + case MAX77693_CHARGING_FAST_CONST_CURRENT: + case MAX77693_CHARGING_FAST_CONST_VOLTAGE: + case MAX77693_CHARGING_TOP_OFF: + /* In high temp the charging current is reduced, but still charging */ + case MAX77693_CHARGING_HIGH_TEMP: + *val = POWER_SUPPLY_STATUS_CHARGING; + break; + case MAX77693_CHARGING_DONE: + *val = POWER_SUPPLY_STATUS_FULL; + break; + case MAX77693_CHARGING_TIMER_EXPIRED: + case MAX77693_CHARGING_THERMISTOR_SUSPEND: + *val = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case MAX77693_CHARGING_OFF: + case MAX77693_CHARGING_OVER_TEMP: + case MAX77693_CHARGING_WATCHDOG_EXPIRED: + *val = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case MAX77693_CHARGING_RESERVED: + default: + *val = POWER_SUPPLY_STATUS_UNKNOWN; + } + + return 0; +} + +static int max77693_get_charge_type(struct regmap *regmap, int *val) +{ + int ret; + unsigned int data; + + ret = regmap_read(regmap, MAX77693_CHG_REG_CHG_DETAILS_01, &data); + if (ret < 0) + return ret; + + data &= CHG_DETAILS_01_CHG_MASK; + data >>= CHG_DETAILS_01_CHG_SHIFT; + + switch (data) { + case MAX77693_CHARGING_PREQUALIFICATION: + /* + * Top-off: trickle or fast? In top-off the current varies between + * 100 and 250 mA. It is higher than prequalification current. + */ + case MAX77693_CHARGING_TOP_OFF: + *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case MAX77693_CHARGING_FAST_CONST_CURRENT: + case MAX77693_CHARGING_FAST_CONST_VOLTAGE: + /* In high temp the charging current is reduced, but still charging */ + case MAX77693_CHARGING_HIGH_TEMP: + *val = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case MAX77693_CHARGING_DONE: + case MAX77693_CHARGING_TIMER_EXPIRED: + case MAX77693_CHARGING_THERMISTOR_SUSPEND: + case MAX77693_CHARGING_OFF: + case MAX77693_CHARGING_OVER_TEMP: + case MAX77693_CHARGING_WATCHDOG_EXPIRED: + *val = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + case MAX77693_CHARGING_RESERVED: + default: + *val = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + } + + return 0; +} + +/* + * Supported health statuses: + * - POWER_SUPPLY_HEALTH_DEAD + * - POWER_SUPPLY_HEALTH_GOOD + * - POWER_SUPPLY_HEALTH_OVERVOLTAGE + * - POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE + * - POWER_SUPPLY_HEALTH_UNKNOWN + * - POWER_SUPPLY_HEALTH_UNSPEC_FAILURE + */ +static int max77693_get_battery_health(struct regmap *regmap, int *val) +{ + int ret; + unsigned int data; + + ret = regmap_read(regmap, MAX77693_CHG_REG_CHG_DETAILS_01, &data); + if (ret < 0) + return ret; + + data &= CHG_DETAILS_01_BAT_MASK; + data >>= CHG_DETAILS_01_BAT_SHIFT; + + switch (data) { + case MAX77693_BATTERY_NOBAT: + *val = POWER_SUPPLY_HEALTH_DEAD; + break; + case MAX77693_BATTERY_PREQUALIFICATION: + case MAX77693_BATTERY_GOOD: + case MAX77693_BATTERY_LOWVOLTAGE: + *val = POWER_SUPPLY_HEALTH_GOOD; + break; + case MAX77693_BATTERY_TIMER_EXPIRED: + /* + * Took longer to charge than expected, charging suspended. + * Damaged battery? + */ + *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + break; + case MAX77693_BATTERY_OVERVOLTAGE: + *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + break; + case MAX77693_BATTERY_OVERCURRENT: + *val = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + case MAX77693_BATTERY_RESERVED: + default: + *val = POWER_SUPPLY_HEALTH_UNKNOWN; + break; + } + + return 0; +} + +static int max77693_get_present(struct regmap *regmap, int *val) +{ + unsigned int data; + int ret; + + /* + * Read CHG_INT_OK register. High DETBAT bit here should be + * equal to value 0x0 in CHG_DETAILS_01/BAT field. + */ + ret = regmap_read(regmap, MAX77693_CHG_REG_CHG_INT_OK, &data); + if (ret < 0) + return ret; + + *val = (data & CHG_INT_OK_DETBAT_MASK) ? 0 : 1; + + return 0; +} + +static int max77693_get_online(struct regmap *regmap, int *val) +{ + unsigned int data; + int ret; + + ret = regmap_read(regmap, MAX77693_CHG_REG_CHG_INT_OK, &data); + if (ret < 0) + return ret; + + *val = (data & CHG_INT_OK_CHGIN_MASK) ? 1 : 0; + + return 0; +} + +static enum power_supply_property max77693_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static int max77693_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max77693_charger *chg = power_supply_get_drvdata(psy); + struct regmap *regmap = chg->max77693->regmap; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = max77693_get_charger_state(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = max77693_get_charge_type(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = max77693_get_battery_health(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = max77693_get_present(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = max77693_get_online(regmap, &val->intval); + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = max77693_charger_model; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = max77693_charger_manufacturer; + break; + default: + return -EINVAL; + } + + return ret; +} + +static const struct power_supply_desc max77693_charger_desc = { + .name = MAX77693_CHARGER_NAME, + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = max77693_charger_props, + .num_properties = ARRAY_SIZE(max77693_charger_props), + .get_property = max77693_charger_get_property, +}; + +static ssize_t device_attr_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count, + int (*fn)(struct max77693_charger *, unsigned long)) +{ + struct max77693_charger *chg = dev_get_drvdata(dev); + unsigned long val; + int ret; + + ret = kstrtoul(buf, 10, &val); + if (ret) + return ret; + + ret = fn(chg, val); + if (ret) + return ret; + + return count; +} + +static ssize_t fast_charge_timer_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct max77693_charger *chg = dev_get_drvdata(dev); + unsigned int data, val; + int ret; + + ret = regmap_read(chg->max77693->regmap, MAX77693_CHG_REG_CHG_CNFG_01, + &data); + if (ret < 0) + return ret; + + data &= CHG_CNFG_01_FCHGTIME_MASK; + data >>= CHG_CNFG_01_FCHGTIME_SHIFT; + switch (data) { + case 0x1 ... 0x7: + /* Starting from 4 hours, step by 2 hours */ + val = 4 + (data - 1) * 2; + break; + case 0x0: + default: + val = 0; + break; + } + + return scnprintf(buf, PAGE_SIZE, "%u\n", val); +} + +static int max77693_set_fast_charge_timer(struct max77693_charger *chg, + unsigned long hours) +{ + unsigned int data; + + /* + * 0x00 - disable + * 0x01 - 4h + * 0x02 - 6h + * ... + * 0x07 - 16h + * Round down odd values. + */ + switch (hours) { + case 4 ... 16: + data = (hours - 4) / 2 + 1; + break; + case 0: + /* Disable */ + data = 0; + break; + default: + return -EINVAL; + } + data <<= CHG_CNFG_01_FCHGTIME_SHIFT; + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_01, + CHG_CNFG_01_FCHGTIME_MASK, data); +} + +static ssize_t fast_charge_timer_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return device_attr_store(dev, attr, buf, count, + max77693_set_fast_charge_timer); +} + +static ssize_t top_off_threshold_current_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct max77693_charger *chg = dev_get_drvdata(dev); + unsigned int data, val; + int ret; + + ret = regmap_read(chg->max77693->regmap, MAX77693_CHG_REG_CHG_CNFG_03, + &data); + if (ret < 0) + return ret; + + data &= CHG_CNFG_03_TOITH_MASK; + data >>= CHG_CNFG_03_TOITH_SHIFT; + + if (data <= 0x04) + val = 100000 + data * 25000; + else + val = data * 50000; + + return scnprintf(buf, PAGE_SIZE, "%u\n", val); +} + +static int max77693_set_top_off_threshold_current(struct max77693_charger *chg, + unsigned long uamp) +{ + unsigned int data; + + if (uamp < 100000 || uamp > 350000) + return -EINVAL; + + if (uamp <= 200000) + data = (uamp - 100000) / 25000; + else + /* (200000, 350000> */ + data = uamp / 50000; + + data <<= CHG_CNFG_03_TOITH_SHIFT; + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_03, + CHG_CNFG_03_TOITH_MASK, data); +} + +static ssize_t top_off_threshold_current_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return device_attr_store(dev, attr, buf, count, + max77693_set_top_off_threshold_current); +} + +static ssize_t top_off_timer_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct max77693_charger *chg = dev_get_drvdata(dev); + unsigned int data, val; + int ret; + + ret = regmap_read(chg->max77693->regmap, MAX77693_CHG_REG_CHG_CNFG_03, + &data); + if (ret < 0) + return ret; + + data &= CHG_CNFG_03_TOTIME_MASK; + data >>= CHG_CNFG_03_TOTIME_SHIFT; + + val = data * 10; + + return scnprintf(buf, PAGE_SIZE, "%u\n", val); +} + +static int max77693_set_top_off_timer(struct max77693_charger *chg, + unsigned long minutes) +{ + unsigned int data; + + if (minutes > 70) + return -EINVAL; + + data = minutes / 10; + data <<= CHG_CNFG_03_TOTIME_SHIFT; + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_03, + CHG_CNFG_03_TOTIME_MASK, data); +} + +static ssize_t top_off_timer_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return device_attr_store(dev, attr, buf, count, + max77693_set_top_off_timer); +} + +static DEVICE_ATTR_RW(fast_charge_timer); +static DEVICE_ATTR_RW(top_off_threshold_current); +static DEVICE_ATTR_RW(top_off_timer); + +static int max77693_set_constant_volt(struct max77693_charger *chg, + unsigned int uvolt) +{ + unsigned int data; + + /* + * 0x00 - 3.650 V + * 0x01 - 3.675 V + * ... + * 0x1b - 4.325 V + * 0x1c - 4.340 V + * 0x1d - 4.350 V + * 0x1e - 4.375 V + * 0x1f - 4.400 V + */ + if (uvolt >= 3650000 && uvolt < 4340000) + data = (uvolt - 3650000) / 25000; + else if (uvolt >= 4340000 && uvolt < 4350000) + data = 0x1c; + else if (uvolt >= 4350000 && uvolt <= 4400000) + data = 0x1d + (uvolt - 4350000) / 25000; + else { + dev_err(chg->dev, "Wrong value for charging constant voltage\n"); + return -EINVAL; + } + + data <<= CHG_CNFG_04_CHGCVPRM_SHIFT; + + dev_dbg(chg->dev, "Charging constant voltage: %u (0x%x)\n", uvolt, + data); + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_04, + CHG_CNFG_04_CHGCVPRM_MASK, data); +} + +static int max77693_set_min_system_volt(struct max77693_charger *chg, + unsigned int uvolt) +{ + unsigned int data; + + if (uvolt < 3000000 || uvolt > 3700000) { + dev_err(chg->dev, "Wrong value for minimum system regulation voltage\n"); + return -EINVAL; + } + + data = (uvolt - 3000000) / 100000; + + data <<= CHG_CNFG_04_MINVSYS_SHIFT; + + dev_dbg(chg->dev, "Minimum system regulation voltage: %u (0x%x)\n", + uvolt, data); + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_04, + CHG_CNFG_04_MINVSYS_MASK, data); +} + +static int max77693_set_thermal_regulation_temp(struct max77693_charger *chg, + unsigned int cels) +{ + unsigned int data; + + switch (cels) { + case 70: + case 85: + case 100: + case 115: + data = (cels - 70) / 15; + break; + default: + dev_err(chg->dev, "Wrong value for thermal regulation loop temperature\n"); + return -EINVAL; + } + + data <<= CHG_CNFG_07_REGTEMP_SHIFT; + + dev_dbg(chg->dev, "Thermal regulation loop temperature: %u (0x%x)\n", + cels, data); + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_07, + CHG_CNFG_07_REGTEMP_MASK, data); +} + +static int max77693_set_batttery_overcurrent(struct max77693_charger *chg, + unsigned int uamp) +{ + unsigned int data; + + if (uamp && (uamp < 2000000 || uamp > 3500000)) { + dev_err(chg->dev, "Wrong value for battery overcurrent\n"); + return -EINVAL; + } + + if (uamp) + data = ((uamp - 2000000) / 250000) + 1; + else + data = 0; /* disable */ + + data <<= CHG_CNFG_12_B2SOVRC_SHIFT; + + dev_dbg(chg->dev, "Battery overcurrent: %u (0x%x)\n", uamp, data); + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_12, + CHG_CNFG_12_B2SOVRC_MASK, data); +} + +static int max77693_set_charge_input_threshold_volt(struct max77693_charger *chg, + unsigned int uvolt) +{ + unsigned int data; + + switch (uvolt) { + case 4300000: + data = 0x0; + break; + case 4700000: + case 4800000: + case 4900000: + data = (uvolt - 4700000) / 100000; + break; + default: + dev_err(chg->dev, "Wrong value for charge input voltage regulation threshold\n"); + return -EINVAL; + } + + data <<= CHG_CNFG_12_VCHGINREG_SHIFT; + + dev_dbg(chg->dev, "Charge input voltage regulation threshold: %u (0x%x)\n", + uvolt, data); + + return regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_12, + CHG_CNFG_12_VCHGINREG_MASK, data); +} + +/* + * Sets charger registers to proper and safe default values. + */ +static int max77693_reg_init(struct max77693_charger *chg) +{ + int ret; + unsigned int data; + + /* Unlock charger register protection */ + data = (0x3 << CHG_CNFG_06_CHGPROT_SHIFT); + ret = regmap_update_bits(chg->max77693->regmap, + MAX77693_CHG_REG_CHG_CNFG_06, + CHG_CNFG_06_CHGPROT_MASK, data); + if (ret) { + dev_err(chg->dev, "Error unlocking registers: %d\n", ret); + return ret; + } + + ret = max77693_set_fast_charge_timer(chg, DEFAULT_FAST_CHARGE_TIMER); + if (ret) + return ret; + + ret = max77693_set_top_off_threshold_current(chg, + DEFAULT_TOP_OFF_THRESHOLD_CURRENT); + if (ret) + return ret; + + ret = max77693_set_top_off_timer(chg, DEFAULT_TOP_OFF_TIMER); + if (ret) + return ret; + + ret = max77693_set_constant_volt(chg, chg->constant_volt); + if (ret) + return ret; + + ret = max77693_set_min_system_volt(chg, chg->min_system_volt); + if (ret) + return ret; + + ret = max77693_set_thermal_regulation_temp(chg, + chg->thermal_regulation_temp); + if (ret) + return ret; + + ret = max77693_set_batttery_overcurrent(chg, chg->batttery_overcurrent); + if (ret) + return ret; + + return max77693_set_charge_input_threshold_volt(chg, + chg->charge_input_threshold_volt); +} + +#ifdef CONFIG_OF +static int max77693_dt_init(struct device *dev, struct max77693_charger *chg) +{ + struct device_node *np = dev->of_node; + + if (!np) { + dev_err(dev, "no charger OF node\n"); + return -EINVAL; + } + + if (of_property_read_u32(np, "maxim,constant-microvolt", + &chg->constant_volt)) + chg->constant_volt = DEFAULT_CONSTANT_VOLT; + + if (of_property_read_u32(np, "maxim,min-system-microvolt", + &chg->min_system_volt)) + chg->min_system_volt = DEFAULT_MIN_SYSTEM_VOLT; + + if (of_property_read_u32(np, "maxim,thermal-regulation-celsius", + &chg->thermal_regulation_temp)) + chg->thermal_regulation_temp = DEFAULT_THERMAL_REGULATION_TEMP; + + if (of_property_read_u32(np, "maxim,battery-overcurrent-microamp", + &chg->batttery_overcurrent)) + chg->batttery_overcurrent = DEFAULT_BATTERY_OVERCURRENT; + + if (of_property_read_u32(np, "maxim,charge-input-threshold-microvolt", + &chg->charge_input_threshold_volt)) + chg->charge_input_threshold_volt = + DEFAULT_CHARGER_INPUT_THRESHOLD_VOLT; + + return 0; +} +#else /* CONFIG_OF */ +static int max77693_dt_init(struct device *dev, struct max77693_charger *chg) +{ + return 0; +} +#endif /* CONFIG_OF */ + +static int max77693_charger_probe(struct platform_device *pdev) +{ + struct max77693_charger *chg; + struct power_supply_config psy_cfg = {}; + struct max77693_dev *max77693 = dev_get_drvdata(pdev->dev.parent); + int ret; + + chg = devm_kzalloc(&pdev->dev, sizeof(*chg), GFP_KERNEL); + if (!chg) + return -ENOMEM; + + platform_set_drvdata(pdev, chg); + chg->dev = &pdev->dev; + chg->max77693 = max77693; + + ret = max77693_dt_init(&pdev->dev, chg); + if (ret) + return ret; + + ret = max77693_reg_init(chg); + if (ret) + return ret; + + psy_cfg.drv_data = chg; + + ret = device_create_file(&pdev->dev, &dev_attr_fast_charge_timer); + if (ret) { + dev_err(&pdev->dev, "failed: create fast charge timer sysfs entry\n"); + goto err; + } + + ret = device_create_file(&pdev->dev, + &dev_attr_top_off_threshold_current); + if (ret) { + dev_err(&pdev->dev, "failed: create top off current sysfs entry\n"); + goto err; + } + + ret = device_create_file(&pdev->dev, &dev_attr_top_off_timer); + if (ret) { + dev_err(&pdev->dev, "failed: create top off timer sysfs entry\n"); + goto err; + } + + chg->charger = power_supply_register(&pdev->dev, + &max77693_charger_desc, + &psy_cfg); + if (IS_ERR(chg->charger)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + ret = PTR_ERR(chg->charger); + goto err; + } + + return 0; + +err: + device_remove_file(&pdev->dev, &dev_attr_top_off_timer); + device_remove_file(&pdev->dev, &dev_attr_top_off_threshold_current); + device_remove_file(&pdev->dev, &dev_attr_fast_charge_timer); + + return ret; +} + +static int max77693_charger_remove(struct platform_device *pdev) +{ + struct max77693_charger *chg = platform_get_drvdata(pdev); + + device_remove_file(&pdev->dev, &dev_attr_top_off_timer); + device_remove_file(&pdev->dev, &dev_attr_top_off_threshold_current); + device_remove_file(&pdev->dev, &dev_attr_fast_charge_timer); + + power_supply_unregister(chg->charger); + + return 0; +} + +static const struct platform_device_id max77693_charger_id[] = { + { "max77693-charger", 0, }, + { } +}; +MODULE_DEVICE_TABLE(platform, max77693_charger_id); + +static struct platform_driver max77693_charger_driver = { + .driver = { + .name = "max77693-charger", + }, + .probe = max77693_charger_probe, + .remove = max77693_charger_remove, + .id_table = max77693_charger_id, +}; +module_platform_driver(max77693_charger_driver); + +MODULE_AUTHOR("Krzysztof Kozlowski <krzk@kernel.org>"); +MODULE_DESCRIPTION("Maxim 77693 charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max8903_charger.c b/drivers/power/supply/max8903_charger.c new file mode 100644 index 000000000..0bd39b0cc --- /dev/null +++ b/drivers/power/supply/max8903_charger.c @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * max8903_charger.c - Maxim 8903 USB/Adapter Charger Driver + * + * Copyright (C) 2011 Samsung Electronics + * MyungJoo Ham <myungjoo.ham@samsung.com> + */ + +#include <linux/gpio.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/slab.h> +#include <linux/power_supply.h> +#include <linux/platform_device.h> +#include <linux/power/max8903_charger.h> + +struct max8903_data { + struct max8903_pdata *pdata; + struct device *dev; + struct power_supply *psy; + struct power_supply_desc psy_desc; + bool fault; + bool usb_in; + bool ta_in; +}; + +static enum power_supply_property max8903_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, /* Charger status output */ + POWER_SUPPLY_PROP_ONLINE, /* External power source */ + POWER_SUPPLY_PROP_HEALTH, /* Fault or OK */ +}; + +static int max8903_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max8903_data *data = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + if (gpio_is_valid(data->pdata->chg)) { + if (gpio_get_value(data->pdata->chg) == 0) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (data->usb_in || data->ta_in) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + } + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = 0; + if (data->usb_in || data->ta_in) + val->intval = 1; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + if (data->fault) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + default: + return -EINVAL; + } + + return 0; +} + +static irqreturn_t max8903_dcin(int irq, void *_data) +{ + struct max8903_data *data = _data; + struct max8903_pdata *pdata = data->pdata; + bool ta_in; + enum power_supply_type old_type; + + ta_in = gpio_get_value(pdata->dok) ? false : true; + + if (ta_in == data->ta_in) + return IRQ_HANDLED; + + data->ta_in = ta_in; + + /* Set Current-Limit-Mode 1:DC 0:USB */ + if (gpio_is_valid(pdata->dcm)) + gpio_set_value(pdata->dcm, ta_in ? 1 : 0); + + /* Charger Enable / Disable (cen is negated) */ + if (gpio_is_valid(pdata->cen)) + gpio_set_value(pdata->cen, ta_in ? 0 : + (data->usb_in ? 0 : 1)); + + dev_dbg(data->dev, "TA(DC-IN) Charger %s.\n", ta_in ? + "Connected" : "Disconnected"); + + old_type = data->psy_desc.type; + + if (data->ta_in) + data->psy_desc.type = POWER_SUPPLY_TYPE_MAINS; + else if (data->usb_in) + data->psy_desc.type = POWER_SUPPLY_TYPE_USB; + else + data->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY; + + if (old_type != data->psy_desc.type) + power_supply_changed(data->psy); + + return IRQ_HANDLED; +} + +static irqreturn_t max8903_usbin(int irq, void *_data) +{ + struct max8903_data *data = _data; + struct max8903_pdata *pdata = data->pdata; + bool usb_in; + enum power_supply_type old_type; + + usb_in = gpio_get_value(pdata->uok) ? false : true; + + if (usb_in == data->usb_in) + return IRQ_HANDLED; + + data->usb_in = usb_in; + + /* Do not touch Current-Limit-Mode */ + + /* Charger Enable / Disable (cen is negated) */ + if (gpio_is_valid(pdata->cen)) + gpio_set_value(pdata->cen, usb_in ? 0 : + (data->ta_in ? 0 : 1)); + + dev_dbg(data->dev, "USB Charger %s.\n", usb_in ? + "Connected" : "Disconnected"); + + old_type = data->psy_desc.type; + + if (data->ta_in) + data->psy_desc.type = POWER_SUPPLY_TYPE_MAINS; + else if (data->usb_in) + data->psy_desc.type = POWER_SUPPLY_TYPE_USB; + else + data->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY; + + if (old_type != data->psy_desc.type) + power_supply_changed(data->psy); + + return IRQ_HANDLED; +} + +static irqreturn_t max8903_fault(int irq, void *_data) +{ + struct max8903_data *data = _data; + struct max8903_pdata *pdata = data->pdata; + bool fault; + + fault = gpio_get_value(pdata->flt) ? false : true; + + if (fault == data->fault) + return IRQ_HANDLED; + + data->fault = fault; + + if (fault) + dev_err(data->dev, "Charger suffers a fault and stops.\n"); + else + dev_err(data->dev, "Charger recovered from a fault.\n"); + + return IRQ_HANDLED; +} + +static struct max8903_pdata *max8903_parse_dt_data(struct device *dev) +{ + struct device_node *np = dev->of_node; + struct max8903_pdata *pdata = NULL; + + if (!np) + return NULL; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return NULL; + + pdata->dc_valid = false; + pdata->usb_valid = false; + + pdata->cen = of_get_named_gpio(np, "cen-gpios", 0); + if (!gpio_is_valid(pdata->cen)) + pdata->cen = -EINVAL; + + pdata->chg = of_get_named_gpio(np, "chg-gpios", 0); + if (!gpio_is_valid(pdata->chg)) + pdata->chg = -EINVAL; + + pdata->flt = of_get_named_gpio(np, "flt-gpios", 0); + if (!gpio_is_valid(pdata->flt)) + pdata->flt = -EINVAL; + + pdata->usus = of_get_named_gpio(np, "usus-gpios", 0); + if (!gpio_is_valid(pdata->usus)) + pdata->usus = -EINVAL; + + pdata->dcm = of_get_named_gpio(np, "dcm-gpios", 0); + if (!gpio_is_valid(pdata->dcm)) + pdata->dcm = -EINVAL; + + pdata->dok = of_get_named_gpio(np, "dok-gpios", 0); + if (!gpio_is_valid(pdata->dok)) + pdata->dok = -EINVAL; + else + pdata->dc_valid = true; + + pdata->uok = of_get_named_gpio(np, "uok-gpios", 0); + if (!gpio_is_valid(pdata->uok)) + pdata->uok = -EINVAL; + else + pdata->usb_valid = true; + + return pdata; +} + +static int max8903_setup_gpios(struct platform_device *pdev) +{ + struct max8903_data *data = platform_get_drvdata(pdev); + struct device *dev = &pdev->dev; + struct max8903_pdata *pdata = pdev->dev.platform_data; + int ret = 0; + int gpio; + int ta_in = 0; + int usb_in = 0; + + if (pdata->dc_valid) { + if (gpio_is_valid(pdata->dok)) { + ret = devm_gpio_request(dev, pdata->dok, + data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for dok: %d err %d\n", + pdata->dok, ret); + return ret; + } + + gpio = pdata->dok; /* PULL_UPed Interrupt */ + ta_in = gpio_get_value(gpio) ? 0 : 1; + } else { + dev_err(dev, "When DC is wired, DOK should be wired as well.\n"); + return -EINVAL; + } + } + + if (gpio_is_valid(pdata->dcm)) { + ret = devm_gpio_request(dev, pdata->dcm, data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for dcm: %d err %d\n", + pdata->dcm, ret); + return ret; + } + + gpio = pdata->dcm; /* Output */ + gpio_set_value(gpio, ta_in); + } + + if (pdata->usb_valid) { + if (gpio_is_valid(pdata->uok)) { + ret = devm_gpio_request(dev, pdata->uok, + data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for uok: %d err %d\n", + pdata->uok, ret); + return ret; + } + + gpio = pdata->uok; + usb_in = gpio_get_value(gpio) ? 0 : 1; + } else { + dev_err(dev, "When USB is wired, UOK should be wired." + "as well.\n"); + return -EINVAL; + } + } + + if (gpio_is_valid(pdata->cen)) { + ret = devm_gpio_request(dev, pdata->cen, data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for cen: %d err %d\n", + pdata->cen, ret); + return ret; + } + + gpio_set_value(pdata->cen, (ta_in || usb_in) ? 0 : 1); + } + + if (gpio_is_valid(pdata->chg)) { + ret = devm_gpio_request(dev, pdata->chg, data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for chg: %d err %d\n", + pdata->chg, ret); + return ret; + } + } + + if (gpio_is_valid(pdata->flt)) { + ret = devm_gpio_request(dev, pdata->flt, data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for flt: %d err %d\n", + pdata->flt, ret); + return ret; + } + } + + if (gpio_is_valid(pdata->usus)) { + ret = devm_gpio_request(dev, pdata->usus, data->psy_desc.name); + if (ret) { + dev_err(dev, + "Failed GPIO request for usus: %d err %d\n", + pdata->usus, ret); + return ret; + } + } + + data->fault = false; + data->ta_in = ta_in; + data->usb_in = usb_in; + + return 0; +} + +static int max8903_probe(struct platform_device *pdev) +{ + struct max8903_data *data; + struct device *dev = &pdev->dev; + struct max8903_pdata *pdata = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + int ret = 0; + + data = devm_kzalloc(dev, sizeof(struct max8903_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (IS_ENABLED(CONFIG_OF) && !pdata && dev->of_node) + pdata = max8903_parse_dt_data(dev); + + if (!pdata) { + dev_err(dev, "No platform data.\n"); + return -EINVAL; + } + + pdev->dev.platform_data = pdata; + data->pdata = pdata; + data->dev = dev; + platform_set_drvdata(pdev, data); + + if (pdata->dc_valid == false && pdata->usb_valid == false) { + dev_err(dev, "No valid power sources.\n"); + return -EINVAL; + } + + ret = max8903_setup_gpios(pdev); + if (ret) + return ret; + + data->psy_desc.name = "max8903_charger"; + data->psy_desc.type = (data->ta_in) ? POWER_SUPPLY_TYPE_MAINS : + ((data->usb_in) ? POWER_SUPPLY_TYPE_USB : + POWER_SUPPLY_TYPE_BATTERY); + data->psy_desc.get_property = max8903_get_property; + data->psy_desc.properties = max8903_charger_props; + data->psy_desc.num_properties = ARRAY_SIZE(max8903_charger_props); + + psy_cfg.of_node = dev->of_node; + psy_cfg.drv_data = data; + + data->psy = devm_power_supply_register(dev, &data->psy_desc, &psy_cfg); + if (IS_ERR(data->psy)) { + dev_err(dev, "failed: power supply register.\n"); + return PTR_ERR(data->psy); + } + + if (pdata->dc_valid) { + ret = devm_request_threaded_irq(dev, gpio_to_irq(pdata->dok), + NULL, max8903_dcin, + IRQF_TRIGGER_FALLING | + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + "MAX8903 DC IN", data); + if (ret) { + dev_err(dev, "Cannot request irq %d for DC (%d)\n", + gpio_to_irq(pdata->dok), ret); + return ret; + } + } + + if (pdata->usb_valid) { + ret = devm_request_threaded_irq(dev, gpio_to_irq(pdata->uok), + NULL, max8903_usbin, + IRQF_TRIGGER_FALLING | + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + "MAX8903 USB IN", data); + if (ret) { + dev_err(dev, "Cannot request irq %d for USB (%d)\n", + gpio_to_irq(pdata->uok), ret); + return ret; + } + } + + if (gpio_is_valid(pdata->flt)) { + ret = devm_request_threaded_irq(dev, gpio_to_irq(pdata->flt), + NULL, max8903_fault, + IRQF_TRIGGER_FALLING | + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + "MAX8903 Fault", data); + if (ret) { + dev_err(dev, "Cannot request irq %d for Fault (%d)\n", + gpio_to_irq(pdata->flt), ret); + return ret; + } + } + + return 0; +} + +static const struct of_device_id max8903_match_ids[] = { + { .compatible = "maxim,max8903", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, max8903_match_ids); + +static struct platform_driver max8903_driver = { + .probe = max8903_probe, + .driver = { + .name = "max8903-charger", + .of_match_table = max8903_match_ids + }, +}; + +module_platform_driver(max8903_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("MAX8903 Charger Driver"); +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_ALIAS("platform:max8903-charger"); diff --git a/drivers/power/supply/max8925_power.c b/drivers/power/supply/max8925_power.c new file mode 100644 index 000000000..8878f9131 --- /dev/null +++ b/drivers/power/supply/max8925_power.c @@ -0,0 +1,594 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for Maxim MAX8925 + * + * Copyright (c) 2009-2010 Marvell International Ltd. + * Haojian Zhuang <haojian.zhuang@marvell.com> + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/of.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/max8925.h> + +/* registers in GPM */ +#define MAX8925_OUT5VEN 0x54 +#define MAX8925_OUT3VEN 0x58 +#define MAX8925_CHG_CNTL1 0x7c + +/* bits definition */ +#define MAX8925_CHG_STAT_VSYSLOW (1 << 0) +#define MAX8925_CHG_STAT_MODE_MASK (3 << 2) +#define MAX8925_CHG_STAT_EN_MASK (1 << 4) +#define MAX8925_CHG_MBDET (1 << 1) +#define MAX8925_CHG_AC_RANGE_MASK (3 << 6) + +/* registers in ADC */ +#define MAX8925_ADC_RES_CNFG1 0x06 +#define MAX8925_ADC_AVG_CNFG1 0x07 +#define MAX8925_ADC_ACQ_CNFG1 0x08 +#define MAX8925_ADC_ACQ_CNFG2 0x09 +/* 2 bytes registers in below. MSB is 1st, LSB is 2nd. */ +#define MAX8925_ADC_AUX2 0x62 +#define MAX8925_ADC_VCHG 0x64 +#define MAX8925_ADC_VBBATT 0x66 +#define MAX8925_ADC_VMBATT 0x68 +#define MAX8925_ADC_ISNS 0x6a +#define MAX8925_ADC_THM 0x6c +#define MAX8925_ADC_TDIE 0x6e +#define MAX8925_CMD_AUX2 0xc8 +#define MAX8925_CMD_VCHG 0xd0 +#define MAX8925_CMD_VBBATT 0xd8 +#define MAX8925_CMD_VMBATT 0xe0 +#define MAX8925_CMD_ISNS 0xe8 +#define MAX8925_CMD_THM 0xf0 +#define MAX8925_CMD_TDIE 0xf8 + +enum { + MEASURE_AUX2, + MEASURE_VCHG, + MEASURE_VBBATT, + MEASURE_VMBATT, + MEASURE_ISNS, + MEASURE_THM, + MEASURE_TDIE, + MEASURE_MAX, +}; + +struct max8925_power_info { + struct max8925_chip *chip; + struct i2c_client *gpm; + struct i2c_client *adc; + + struct power_supply *ac; + struct power_supply *usb; + struct power_supply *battery; + int irq_base; + unsigned ac_online:1; + unsigned usb_online:1; + unsigned bat_online:1; + unsigned chg_mode:2; + unsigned batt_detect:1; /* detecing MB by ID pin */ + unsigned topoff_threshold:2; + unsigned fast_charge:3; + unsigned no_temp_support:1; + unsigned no_insert_detect:1; + + int (*set_charger) (int); +}; + +static int __set_charger(struct max8925_power_info *info, int enable) +{ + struct max8925_chip *chip = info->chip; + if (enable) { + /* enable charger in platform */ + if (info->set_charger) + info->set_charger(1); + /* enable charger */ + max8925_set_bits(info->gpm, MAX8925_CHG_CNTL1, 1 << 7, 0); + } else { + /* disable charge */ + max8925_set_bits(info->gpm, MAX8925_CHG_CNTL1, 1 << 7, 1 << 7); + if (info->set_charger) + info->set_charger(0); + } + dev_dbg(chip->dev, "%s\n", (enable) ? "Enable charger" + : "Disable charger"); + return 0; +} + +static irqreturn_t max8925_charger_handler(int irq, void *data) +{ + struct max8925_power_info *info = (struct max8925_power_info *)data; + struct max8925_chip *chip = info->chip; + + switch (irq - chip->irq_base) { + case MAX8925_IRQ_VCHG_DC_R: + info->ac_online = 1; + __set_charger(info, 1); + dev_dbg(chip->dev, "Adapter inserted\n"); + break; + case MAX8925_IRQ_VCHG_DC_F: + info->ac_online = 0; + __set_charger(info, 0); + dev_dbg(chip->dev, "Adapter removed\n"); + break; + case MAX8925_IRQ_VCHG_THM_OK_F: + /* Battery is not ready yet */ + dev_dbg(chip->dev, "Battery temperature is out of range\n"); + fallthrough; + case MAX8925_IRQ_VCHG_DC_OVP: + dev_dbg(chip->dev, "Error detection\n"); + __set_charger(info, 0); + break; + case MAX8925_IRQ_VCHG_THM_OK_R: + /* Battery is ready now */ + dev_dbg(chip->dev, "Battery temperature is in range\n"); + break; + case MAX8925_IRQ_VCHG_SYSLOW_R: + /* VSYS is low */ + dev_info(chip->dev, "Sys power is too low\n"); + break; + case MAX8925_IRQ_VCHG_SYSLOW_F: + dev_dbg(chip->dev, "Sys power is above low threshold\n"); + break; + case MAX8925_IRQ_VCHG_DONE: + __set_charger(info, 0); + dev_dbg(chip->dev, "Charging is done\n"); + break; + case MAX8925_IRQ_VCHG_TOPOFF: + dev_dbg(chip->dev, "Charging in top-off mode\n"); + break; + case MAX8925_IRQ_VCHG_TMR_FAULT: + __set_charger(info, 0); + dev_dbg(chip->dev, "Safe timer is expired\n"); + break; + case MAX8925_IRQ_VCHG_RST: + __set_charger(info, 0); + dev_dbg(chip->dev, "Charger is reset\n"); + break; + } + return IRQ_HANDLED; +} + +static int start_measure(struct max8925_power_info *info, int type) +{ + unsigned char buf[2] = {0, 0}; + int meas_cmd; + int meas_reg = 0, ret; + + switch (type) { + case MEASURE_VCHG: + meas_cmd = MAX8925_CMD_VCHG; + meas_reg = MAX8925_ADC_VCHG; + break; + case MEASURE_VBBATT: + meas_cmd = MAX8925_CMD_VBBATT; + meas_reg = MAX8925_ADC_VBBATT; + break; + case MEASURE_VMBATT: + meas_cmd = MAX8925_CMD_VMBATT; + meas_reg = MAX8925_ADC_VMBATT; + break; + case MEASURE_ISNS: + meas_cmd = MAX8925_CMD_ISNS; + meas_reg = MAX8925_ADC_ISNS; + break; + default: + return -EINVAL; + } + + max8925_reg_write(info->adc, meas_cmd, 0); + max8925_bulk_read(info->adc, meas_reg, 2, buf); + ret = ((buf[0]<<8) | buf[1]) >> 4; + + return ret; +} + +static int max8925_ac_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max8925_power_info *info = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = info->ac_online; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (info->ac_online) { + ret = start_measure(info, MEASURE_VCHG); + if (ret >= 0) { + val->intval = ret * 2000; /* unit is uV */ + goto out; + } + } + ret = -ENODATA; + break; + default: + ret = -ENODEV; + break; + } +out: + return ret; +} + +static enum power_supply_property max8925_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static int max8925_usb_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max8925_power_info *info = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = info->usb_online; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (info->usb_online) { + ret = start_measure(info, MEASURE_VCHG); + if (ret >= 0) { + val->intval = ret * 2000; /* unit is uV */ + goto out; + } + } + ret = -ENODATA; + break; + default: + ret = -ENODEV; + break; + } +out: + return ret; +} + +static enum power_supply_property max8925_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static int max8925_bat_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max8925_power_info *info = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = info->bat_online; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (info->bat_online) { + ret = start_measure(info, MEASURE_VMBATT); + if (ret >= 0) { + val->intval = ret * 2000; /* unit is uV */ + ret = 0; + break; + } + } + ret = -ENODATA; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (info->bat_online) { + ret = start_measure(info, MEASURE_ISNS); + if (ret >= 0) { + /* assume r_sns is 0.02 */ + ret = ((ret * 6250) - 3125) /* uA */; + val->intval = 0; + if (ret > 0) + val->intval = ret; /* unit is mA */ + ret = 0; + break; + } + } + ret = -ENODATA; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + if (!info->bat_online) { + ret = -ENODATA; + break; + } + ret = max8925_reg_read(info->gpm, MAX8925_CHG_STATUS); + ret = (ret & MAX8925_CHG_STAT_MODE_MASK) >> 2; + switch (ret) { + case 1: + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case 0: + case 2: + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case 3: + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + } + ret = 0; + break; + case POWER_SUPPLY_PROP_STATUS: + if (!info->bat_online) { + ret = -ENODATA; + break; + } + ret = max8925_reg_read(info->gpm, MAX8925_CHG_STATUS); + if (info->usb_online || info->ac_online) { + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + if (ret & MAX8925_CHG_STAT_EN_MASK) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + } else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + ret = 0; + break; + default: + ret = -ENODEV; + break; + } + return ret; +} + +static enum power_supply_property max8925_battery_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_STATUS, +}; + +static const struct power_supply_desc ac_desc = { + .name = "max8925-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = max8925_ac_props, + .num_properties = ARRAY_SIZE(max8925_ac_props), + .get_property = max8925_ac_get_prop, +}; + +static const struct power_supply_desc usb_desc = { + .name = "max8925-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = max8925_usb_props, + .num_properties = ARRAY_SIZE(max8925_usb_props), + .get_property = max8925_usb_get_prop, +}; + +static const struct power_supply_desc battery_desc = { + .name = "max8925-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = max8925_battery_props, + .num_properties = ARRAY_SIZE(max8925_battery_props), + .get_property = max8925_bat_get_prop, +}; + +#define REQUEST_IRQ(_irq, _name) \ +do { \ + ret = request_threaded_irq(chip->irq_base + _irq, NULL, \ + max8925_charger_handler, \ + IRQF_ONESHOT, _name, info); \ + if (ret) \ + dev_err(chip->dev, "Failed to request IRQ #%d: %d\n", \ + _irq, ret); \ +} while (0) + +static int max8925_init_charger(struct max8925_chip *chip, + struct max8925_power_info *info) +{ + int ret; + + REQUEST_IRQ(MAX8925_IRQ_VCHG_DC_OVP, "ac-ovp"); + if (!info->no_insert_detect) { + REQUEST_IRQ(MAX8925_IRQ_VCHG_DC_F, "ac-remove"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_DC_R, "ac-insert"); + } + if (!info->no_temp_support) { + REQUEST_IRQ(MAX8925_IRQ_VCHG_THM_OK_R, "batt-temp-in-range"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_THM_OK_F, "batt-temp-out-range"); + } + REQUEST_IRQ(MAX8925_IRQ_VCHG_SYSLOW_F, "vsys-high"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_SYSLOW_R, "vsys-low"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_RST, "charger-reset"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_DONE, "charger-done"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_TOPOFF, "charger-topoff"); + REQUEST_IRQ(MAX8925_IRQ_VCHG_TMR_FAULT, "charger-timer-expire"); + + info->usb_online = 0; + info->bat_online = 0; + + /* check for power - can miss interrupt at boot time */ + if (start_measure(info, MEASURE_VCHG) * 2000 > 500000) + info->ac_online = 1; + else + info->ac_online = 0; + + ret = max8925_reg_read(info->gpm, MAX8925_CHG_STATUS); + if (ret >= 0) { + /* + * If battery detection is enabled, ID pin of battery is + * connected to MBDET pin of MAX8925. It could be used to + * detect battery presence. + * Otherwise, we have to assume that battery is always on. + */ + if (info->batt_detect) + info->bat_online = (ret & MAX8925_CHG_MBDET) ? 0 : 1; + else + info->bat_online = 1; + if (ret & MAX8925_CHG_AC_RANGE_MASK) + info->ac_online = 1; + else + info->ac_online = 0; + } + /* disable charge */ + max8925_set_bits(info->gpm, MAX8925_CHG_CNTL1, 1 << 7, 1 << 7); + /* set charging current in charge topoff mode */ + max8925_set_bits(info->gpm, MAX8925_CHG_CNTL1, 3 << 5, + info->topoff_threshold << 5); + /* set charing current in fast charge mode */ + max8925_set_bits(info->gpm, MAX8925_CHG_CNTL1, 7, info->fast_charge); + + return 0; +} + +static int max8925_deinit_charger(struct max8925_power_info *info) +{ + struct max8925_chip *chip = info->chip; + int irq; + + irq = chip->irq_base + MAX8925_IRQ_VCHG_DC_OVP; + for (; irq <= chip->irq_base + MAX8925_IRQ_VCHG_TMR_FAULT; irq++) + free_irq(irq, info); + + return 0; +} + +#ifdef CONFIG_OF +static struct max8925_power_pdata * +max8925_power_dt_init(struct platform_device *pdev) +{ + struct device_node *nproot = pdev->dev.parent->of_node; + struct device_node *np; + int batt_detect; + int topoff_threshold; + int fast_charge; + int no_temp_support; + int no_insert_detect; + struct max8925_power_pdata *pdata; + + if (!nproot) + return pdev->dev.platform_data; + + np = of_get_child_by_name(nproot, "charger"); + if (!np) { + dev_err(&pdev->dev, "failed to find charger node\n"); + return NULL; + } + + pdata = devm_kzalloc(&pdev->dev, + sizeof(struct max8925_power_pdata), + GFP_KERNEL); + if (!pdata) + goto ret; + + of_property_read_u32(np, "topoff-threshold", &topoff_threshold); + of_property_read_u32(np, "batt-detect", &batt_detect); + of_property_read_u32(np, "fast-charge", &fast_charge); + of_property_read_u32(np, "no-insert-detect", &no_insert_detect); + of_property_read_u32(np, "no-temp-support", &no_temp_support); + + pdata->batt_detect = batt_detect; + pdata->fast_charge = fast_charge; + pdata->topoff_threshold = topoff_threshold; + pdata->no_insert_detect = no_insert_detect; + pdata->no_temp_support = no_temp_support; + +ret: + of_node_put(np); + return pdata; +} +#else +static struct max8925_power_pdata * +max8925_power_dt_init(struct platform_device *pdev) +{ + return pdev->dev.platform_data; +} +#endif + +static int max8925_power_probe(struct platform_device *pdev) +{ + struct max8925_chip *chip = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; /* Only for ac and usb */ + struct max8925_power_pdata *pdata = NULL; + struct max8925_power_info *info; + int ret; + + pdata = max8925_power_dt_init(pdev); + if (!pdata) { + dev_err(&pdev->dev, "platform data isn't assigned to " + "power supply\n"); + return -EINVAL; + } + + info = devm_kzalloc(&pdev->dev, sizeof(struct max8925_power_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + info->chip = chip; + info->gpm = chip->i2c; + info->adc = chip->adc; + platform_set_drvdata(pdev, info); + + psy_cfg.supplied_to = pdata->supplied_to; + psy_cfg.num_supplicants = pdata->num_supplicants; + + info->ac = power_supply_register(&pdev->dev, &ac_desc, &psy_cfg); + if (IS_ERR(info->ac)) { + ret = PTR_ERR(info->ac); + goto out; + } + info->ac->dev.parent = &pdev->dev; + + info->usb = power_supply_register(&pdev->dev, &usb_desc, &psy_cfg); + if (IS_ERR(info->usb)) { + ret = PTR_ERR(info->usb); + goto out_unregister_ac; + } + info->usb->dev.parent = &pdev->dev; + + info->battery = power_supply_register(&pdev->dev, &battery_desc, NULL); + if (IS_ERR(info->battery)) { + ret = PTR_ERR(info->battery); + goto out_unregister_usb; + } + info->battery->dev.parent = &pdev->dev; + + info->batt_detect = pdata->batt_detect; + info->topoff_threshold = pdata->topoff_threshold; + info->fast_charge = pdata->fast_charge; + info->set_charger = pdata->set_charger; + info->no_temp_support = pdata->no_temp_support; + info->no_insert_detect = pdata->no_insert_detect; + + max8925_init_charger(chip, info); + return 0; +out_unregister_usb: + power_supply_unregister(info->usb); +out_unregister_ac: + power_supply_unregister(info->ac); +out: + return ret; +} + +static int max8925_power_remove(struct platform_device *pdev) +{ + struct max8925_power_info *info = platform_get_drvdata(pdev); + + if (info) { + power_supply_unregister(info->ac); + power_supply_unregister(info->usb); + power_supply_unregister(info->battery); + max8925_deinit_charger(info); + } + return 0; +} + +static struct platform_driver max8925_power_driver = { + .probe = max8925_power_probe, + .remove = max8925_power_remove, + .driver = { + .name = "max8925-power", + }, +}; + +module_platform_driver(max8925_power_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Power supply driver for MAX8925"); +MODULE_ALIAS("platform:max8925-power"); diff --git a/drivers/power/supply/max8997_charger.c b/drivers/power/supply/max8997_charger.c new file mode 100644 index 000000000..f5e84cd47 --- /dev/null +++ b/drivers/power/supply/max8997_charger.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// max8997_charger.c - Power supply consumer driver for the Maxim 8997/8966 +// +// Copyright (C) 2011 Samsung Electronics +// MyungJoo Ham <myungjoo.ham@samsung.com> + +#include <linux/err.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/max8997.h> +#include <linux/mfd/max8997-private.h> + +struct charger_data { + struct device *dev; + struct max8997_dev *iodev; + struct power_supply *battery; +}; + +static enum power_supply_property max8997_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, /* "FULL" or "NOT FULL" only. */ + POWER_SUPPLY_PROP_PRESENT, /* the presence of battery */ + POWER_SUPPLY_PROP_ONLINE, /* charger is active or not */ +}; + +/* Note that the charger control is done by a current regulator "CHARGER" */ +static int max8997_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct charger_data *charger = power_supply_get_drvdata(psy); + struct i2c_client *i2c = charger->iodev->i2c; + int ret; + u8 reg; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = 0; + ret = max8997_read_reg(i2c, MAX8997_REG_STATUS4, ®); + if (ret) + return ret; + if ((reg & (1 << 0)) == 0x1) + val->intval = POWER_SUPPLY_STATUS_FULL; + + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 0; + ret = max8997_read_reg(i2c, MAX8997_REG_STATUS4, ®); + if (ret) + return ret; + if ((reg & (1 << 2)) == 0x0) + val->intval = 1; + + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = 0; + ret = max8997_read_reg(i2c, MAX8997_REG_STATUS4, ®); + if (ret) + return ret; + /* DCINOK */ + if (reg & (1 << 1)) + val->intval = 1; + + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct power_supply_desc max8997_battery_desc = { + .name = "max8997_pmic", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = max8997_battery_get_property, + .properties = max8997_battery_props, + .num_properties = ARRAY_SIZE(max8997_battery_props), +}; + +static int max8997_battery_probe(struct platform_device *pdev) +{ + int ret = 0; + struct charger_data *charger; + struct max8997_dev *iodev = dev_get_drvdata(pdev->dev.parent); + struct max8997_platform_data *pdata = dev_get_platdata(iodev->dev); + struct power_supply_config psy_cfg = {}; + + if (!pdata) + return -EINVAL; + + if (pdata->eoc_mA) { + int val = (pdata->eoc_mA - 50) / 10; + if (val < 0) + val = 0; + if (val > 0xf) + val = 0xf; + + ret = max8997_update_reg(iodev->i2c, + MAX8997_REG_MBCCTRL5, val, 0xf); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot use i2c bus.\n"); + return ret; + } + } + + switch (pdata->timeout) { + case 5: + ret = max8997_update_reg(iodev->i2c, MAX8997_REG_MBCCTRL1, + 0x2 << 4, 0x7 << 4); + break; + case 6: + ret = max8997_update_reg(iodev->i2c, MAX8997_REG_MBCCTRL1, + 0x3 << 4, 0x7 << 4); + break; + case 7: + ret = max8997_update_reg(iodev->i2c, MAX8997_REG_MBCCTRL1, + 0x4 << 4, 0x7 << 4); + break; + case 0: + ret = max8997_update_reg(iodev->i2c, MAX8997_REG_MBCCTRL1, + 0x7 << 4, 0x7 << 4); + break; + default: + dev_err(&pdev->dev, "incorrect timeout value (%d)\n", + pdata->timeout); + return -EINVAL; + } + if (ret < 0) { + dev_err(&pdev->dev, "Cannot use i2c bus.\n"); + return ret; + } + + charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + platform_set_drvdata(pdev, charger); + + + charger->dev = &pdev->dev; + charger->iodev = iodev; + + psy_cfg.drv_data = charger; + + charger->battery = devm_power_supply_register(&pdev->dev, + &max8997_battery_desc, + &psy_cfg); + if (IS_ERR(charger->battery)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + return PTR_ERR(charger->battery); + } + + return 0; +} + +static const struct platform_device_id max8997_battery_id[] = { + { "max8997-battery", 0 }, + { } +}; +MODULE_DEVICE_TABLE(platform, max8997_battery_id); + +static struct platform_driver max8997_battery_driver = { + .driver = { + .name = "max8997-battery", + }, + .probe = max8997_battery_probe, + .id_table = max8997_battery_id, +}; + +static int __init max8997_battery_init(void) +{ + return platform_driver_register(&max8997_battery_driver); +} +subsys_initcall(max8997_battery_init); + +static void __exit max8997_battery_cleanup(void) +{ + platform_driver_unregister(&max8997_battery_driver); +} +module_exit(max8997_battery_cleanup); + +MODULE_DESCRIPTION("MAXIM 8997/8966 battery control driver"); +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/max8998_charger.c b/drivers/power/supply/max8998_charger.c new file mode 100644 index 000000000..c26023b19 --- /dev/null +++ b/drivers/power/supply/max8998_charger.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// max8998_charger.c - Power supply consumer driver for the Maxim 8998/LP3974 +// +// Copyright (C) 2009-2010 Samsung Electronics +// MyungJoo Ham <myungjoo.ham@samsung.com> + +#include <linux/err.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/max8998.h> +#include <linux/mfd/max8998-private.h> + +struct max8998_battery_data { + struct device *dev; + struct max8998_dev *iodev; + struct power_supply *battery; +}; + +static enum power_supply_property max8998_battery_props[] = { + POWER_SUPPLY_PROP_PRESENT, /* the presence of battery */ + POWER_SUPPLY_PROP_ONLINE, /* charger is active or not */ + POWER_SUPPLY_PROP_STATUS, /* charger is charging/discharging/full */ +}; + +/* Note that the charger control is done by a current regulator "CHARGER" */ +static int max8998_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct max8998_battery_data *max8998 = power_supply_get_drvdata(psy); + struct i2c_client *i2c = max8998->iodev->i2c; + int ret; + u8 reg; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + ret = max8998_read_reg(i2c, MAX8998_REG_STATUS2, ®); + if (ret) + return ret; + if (reg & (1 << 4)) + val->intval = 0; + else + val->intval = 1; + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = max8998_read_reg(i2c, MAX8998_REG_STATUS2, ®); + if (ret) + return ret; + + if (reg & (1 << 5)) + val->intval = 1; + else + val->intval = 0; + + break; + case POWER_SUPPLY_PROP_STATUS: + ret = max8998_read_reg(i2c, MAX8998_REG_STATUS2, ®); + if (ret) + return ret; + + if (!(reg & (1 << 5))) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + } else { + if (reg & (1 << 6)) + val->intval = POWER_SUPPLY_STATUS_FULL; + else if (reg & (1 << 3)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct power_supply_desc max8998_battery_desc = { + .name = "max8998_pmic", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = max8998_battery_get_property, + .properties = max8998_battery_props, + .num_properties = ARRAY_SIZE(max8998_battery_props), +}; + +static int max8998_battery_probe(struct platform_device *pdev) +{ + struct max8998_dev *iodev = dev_get_drvdata(pdev->dev.parent); + struct max8998_platform_data *pdata = iodev->pdata; + struct power_supply_config psy_cfg = {}; + struct max8998_battery_data *max8998; + struct i2c_client *i2c; + int ret = 0; + + if (!pdata) { + dev_err(pdev->dev.parent, "No platform init data supplied\n"); + return -ENODEV; + } + + max8998 = devm_kzalloc(&pdev->dev, sizeof(struct max8998_battery_data), + GFP_KERNEL); + if (!max8998) + return -ENOMEM; + + max8998->dev = &pdev->dev; + max8998->iodev = iodev; + platform_set_drvdata(pdev, max8998); + i2c = max8998->iodev->i2c; + + /* Setup "End of Charge" */ + /* If EOC value equals 0, + * remain value set from bootloader or default value */ + if (pdata->eoc >= 10 && pdata->eoc <= 45) { + max8998_update_reg(i2c, MAX8998_REG_CHGR1, + (pdata->eoc / 5 - 2) << 5, 0x7 << 5); + } else if (pdata->eoc == 0) { + dev_dbg(max8998->dev, + "EOC value not set: leave it unchanged.\n"); + } else { + dev_err(max8998->dev, "Invalid EOC value\n"); + return -EINVAL; + } + + /* Setup Charge Restart Level */ + switch (pdata->restart) { + case 100: + max8998_update_reg(i2c, MAX8998_REG_CHGR1, 0x1 << 3, 0x3 << 3); + break; + case 150: + max8998_update_reg(i2c, MAX8998_REG_CHGR1, 0x0 << 3, 0x3 << 3); + break; + case 200: + max8998_update_reg(i2c, MAX8998_REG_CHGR1, 0x2 << 3, 0x3 << 3); + break; + case -1: + max8998_update_reg(i2c, MAX8998_REG_CHGR1, 0x3 << 3, 0x3 << 3); + break; + case 0: + dev_dbg(max8998->dev, + "Restart Level not set: leave it unchanged.\n"); + break; + default: + dev_err(max8998->dev, "Invalid Restart Level\n"); + return -EINVAL; + } + + /* Setup Charge Full Timeout */ + switch (pdata->timeout) { + case 5: + max8998_update_reg(i2c, MAX8998_REG_CHGR2, 0x0 << 4, 0x3 << 4); + break; + case 6: + max8998_update_reg(i2c, MAX8998_REG_CHGR2, 0x1 << 4, 0x3 << 4); + break; + case 7: + max8998_update_reg(i2c, MAX8998_REG_CHGR2, 0x2 << 4, 0x3 << 4); + break; + case -1: + max8998_update_reg(i2c, MAX8998_REG_CHGR2, 0x3 << 4, 0x3 << 4); + break; + case 0: + dev_dbg(max8998->dev, + "Full Timeout not set: leave it unchanged.\n"); + break; + default: + dev_err(max8998->dev, "Invalid Full Timeout value\n"); + return -EINVAL; + } + + psy_cfg.drv_data = max8998; + + max8998->battery = devm_power_supply_register(max8998->dev, + &max8998_battery_desc, + &psy_cfg); + if (IS_ERR(max8998->battery)) { + ret = PTR_ERR(max8998->battery); + dev_err(max8998->dev, "failed: power supply register: %d\n", + ret); + return ret; + } + + return 0; +} + +static const struct platform_device_id max8998_battery_id[] = { + { "max8998-battery", TYPE_MAX8998 }, + { } +}; + +static struct platform_driver max8998_battery_driver = { + .driver = { + .name = "max8998-battery", + }, + .probe = max8998_battery_probe, + .id_table = max8998_battery_id, +}; + +module_platform_driver(max8998_battery_driver); + +MODULE_DESCRIPTION("MAXIM 8998 battery control driver"); +MODULE_AUTHOR("MyungJoo Ham <myungjoo.ham@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:max8998-battery"); diff --git a/drivers/power/supply/mp2629_charger.c b/drivers/power/supply/mp2629_charger.c new file mode 100644 index 000000000..bdf924b73 --- /dev/null +++ b/drivers/power/supply/mp2629_charger.c @@ -0,0 +1,669 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MP2629 battery charger driver + * + * Copyright 2020 Monolithic Power Systems, Inc + * + * Author: Saravanan Sekar <sravanhome@gmail.com> + */ + +#include <linux/bits.h> +#include <linux/iio/consumer.h> +#include <linux/iio/types.h> +#include <linux/interrupt.h> +#include <linux/mfd/mp2629.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> + +#define MP2629_REG_INPUT_ILIM 0x00 +#define MP2629_REG_INPUT_VLIM 0x01 +#define MP2629_REG_CHARGE_CTRL 0x04 +#define MP2629_REG_CHARGE_ILIM 0x05 +#define MP2629_REG_PRECHARGE 0x06 +#define MP2629_REG_TERM_CURRENT 0x06 +#define MP2629_REG_CHARGE_VLIM 0x07 +#define MP2629_REG_TIMER_CTRL 0x08 +#define MP2629_REG_IMPEDANCE_COMP 0x09 +#define MP2629_REG_INTERRUPT 0x0b +#define MP2629_REG_STATUS 0x0c +#define MP2629_REG_FAULT 0x0d + +#define MP2629_MASK_INPUT_TYPE GENMASK(7, 5) +#define MP2629_MASK_CHARGE_TYPE GENMASK(4, 3) +#define MP2629_MASK_CHARGE_CTRL GENMASK(5, 4) +#define MP2629_MASK_WDOG_CTRL GENMASK(5, 4) +#define MP2629_MASK_IMPEDANCE GENMASK(7, 4) + +#define MP2629_INPUTSOURCE_CHANGE GENMASK(7, 5) +#define MP2629_CHARGING_CHANGE GENMASK(4, 3) +#define MP2629_FAULT_BATTERY BIT(3) +#define MP2629_FAULT_THERMAL BIT(4) +#define MP2629_FAULT_INPUT BIT(5) +#define MP2629_FAULT_OTG BIT(6) + +#define MP2629_MAX_BATT_CAPACITY 100 + +#define MP2629_PROPS(_idx, _min, _max, _step) \ + [_idx] = { \ + .min = _min, \ + .max = _max, \ + .step = _step, \ +} + +enum mp2629_source_type { + MP2629_SOURCE_TYPE_NO_INPUT, + MP2629_SOURCE_TYPE_NON_STD, + MP2629_SOURCE_TYPE_SDP, + MP2629_SOURCE_TYPE_CDP, + MP2629_SOURCE_TYPE_DCP, + MP2629_SOURCE_TYPE_OTG = 7, +}; + +enum mp2629_field { + INPUT_ILIM, + INPUT_VLIM, + CHARGE_ILIM, + CHARGE_VLIM, + PRECHARGE, + TERM_CURRENT, + MP2629_MAX_FIELD +}; + +struct mp2629_charger { + struct device *dev; + int status; + int fault; + + struct regmap *regmap; + struct regmap_field *regmap_fields[MP2629_MAX_FIELD]; + struct mutex lock; + struct power_supply *usb; + struct power_supply *battery; + struct iio_channel *iiochan[MP2629_ADC_CHAN_END]; +}; + +struct mp2629_prop { + int reg; + int mask; + int min; + int max; + int step; + int shift; +}; + +static enum power_supply_usb_type mp2629_usb_types[] = { + POWER_SUPPLY_USB_TYPE_SDP, + POWER_SUPPLY_USB_TYPE_DCP, + POWER_SUPPLY_USB_TYPE_CDP, + POWER_SUPPLY_USB_TYPE_PD_DRP, + POWER_SUPPLY_USB_TYPE_UNKNOWN +}; + +static enum power_supply_property mp2629_charger_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT, +}; + +static enum power_supply_property mp2629_charger_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_PRECHARGE_CURRENT, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, +}; + +static struct mp2629_prop props[] = { + MP2629_PROPS(INPUT_ILIM, 100000, 3250000, 50000), + MP2629_PROPS(INPUT_VLIM, 3800000, 5300000, 100000), + MP2629_PROPS(CHARGE_ILIM, 320000, 4520000, 40000), + MP2629_PROPS(CHARGE_VLIM, 3400000, 4670000, 10000), + MP2629_PROPS(PRECHARGE, 120000, 720000, 40000), + MP2629_PROPS(TERM_CURRENT, 80000, 680000, 40000), +}; + +static const struct reg_field mp2629_reg_fields[] = { + [INPUT_ILIM] = REG_FIELD(MP2629_REG_INPUT_ILIM, 0, 5), + [INPUT_VLIM] = REG_FIELD(MP2629_REG_INPUT_VLIM, 0, 3), + [CHARGE_ILIM] = REG_FIELD(MP2629_REG_CHARGE_ILIM, 0, 6), + [CHARGE_VLIM] = REG_FIELD(MP2629_REG_CHARGE_VLIM, 1, 7), + [PRECHARGE] = REG_FIELD(MP2629_REG_PRECHARGE, 4, 7), + [TERM_CURRENT] = REG_FIELD(MP2629_REG_TERM_CURRENT, 0, 3), +}; + +static char *adc_chan_name[] = { + "mp2629-batt-volt", + "mp2629-system-volt", + "mp2629-input-volt", + "mp2629-batt-current", + "mp2629-input-current", +}; + +static int mp2629_read_adc(struct mp2629_charger *charger, + enum mp2629_adc_chan ch, + union power_supply_propval *val) +{ + int ret; + int chval; + + ret = iio_read_channel_processed(charger->iiochan[ch], &chval); + if (ret) + return ret; + + val->intval = chval * 1000; + + return 0; +} + +static int mp2629_get_prop(struct mp2629_charger *charger, + enum mp2629_field fld, + union power_supply_propval *val) +{ + int ret; + unsigned int rval; + + ret = regmap_field_read(charger->regmap_fields[fld], &rval); + if (ret) + return ret; + + val->intval = rval * props[fld].step + props[fld].min; + + return 0; +} + +static int mp2629_set_prop(struct mp2629_charger *charger, + enum mp2629_field fld, + const union power_supply_propval *val) +{ + unsigned int rval; + + if (val->intval < props[fld].min || val->intval > props[fld].max) + return -EINVAL; + + rval = (val->intval - props[fld].min) / props[fld].step; + return regmap_field_write(charger->regmap_fields[fld], rval); +} + +static int mp2629_get_battery_capacity(struct mp2629_charger *charger, + union power_supply_propval *val) +{ + union power_supply_propval vnow, vlim; + int ret; + + ret = mp2629_read_adc(charger, MP2629_BATT_VOLT, &vnow); + if (ret) + return ret; + + ret = mp2629_get_prop(charger, CHARGE_VLIM, &vlim); + if (ret) + return ret; + + val->intval = (vnow.intval * 100) / vlim.intval; + val->intval = min(val->intval, MP2629_MAX_BATT_CAPACITY); + + return 0; +} + +static int mp2629_charger_battery_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct mp2629_charger *charger = dev_get_drvdata(psy->dev.parent); + unsigned int rval; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = mp2629_read_adc(charger, MP2629_BATT_VOLT, val); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = mp2629_read_adc(charger, MP2629_BATT_CURRENT, val); + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + val->intval = 4520000; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + val->intval = 4670000; + break; + + case POWER_SUPPLY_PROP_CAPACITY: + ret = mp2629_get_battery_capacity(charger, val); + break; + + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + ret = mp2629_get_prop(charger, TERM_CURRENT, val); + break; + + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + ret = mp2629_get_prop(charger, PRECHARGE, val); + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = mp2629_get_prop(charger, CHARGE_VLIM, val); + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = mp2629_get_prop(charger, CHARGE_ILIM, val); + break; + + case POWER_SUPPLY_PROP_HEALTH: + if (!charger->fault) + val->intval = POWER_SUPPLY_HEALTH_GOOD; + if (MP2629_FAULT_BATTERY & charger->fault) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (MP2629_FAULT_THERMAL & charger->fault) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (MP2629_FAULT_INPUT & charger->fault) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + break; + + case POWER_SUPPLY_PROP_STATUS: + ret = regmap_read(charger->regmap, MP2629_REG_STATUS, &rval); + if (ret) + break; + + rval = (rval & MP2629_MASK_CHARGE_TYPE) >> 3; + switch (rval) { + case 0x00: + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case 0x01: + case 0x10: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + break; + case 0x11: + val->intval = POWER_SUPPLY_STATUS_FULL; + } + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = regmap_read(charger->regmap, MP2629_REG_STATUS, &rval); + if (ret) + break; + + rval = (rval & MP2629_MASK_CHARGE_TYPE) >> 3; + switch (rval) { + case 0x00: + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + case 0x01: + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case 0x10: + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + break; + default: + val->intval = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + } + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int mp2629_charger_battery_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct mp2629_charger *charger = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + return mp2629_set_prop(charger, TERM_CURRENT, val); + + case POWER_SUPPLY_PROP_PRECHARGE_CURRENT: + return mp2629_set_prop(charger, PRECHARGE, val); + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + return mp2629_set_prop(charger, CHARGE_VLIM, val); + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return mp2629_set_prop(charger, CHARGE_ILIM, val); + + default: + return -EINVAL; + } +} + +static int mp2629_charger_usb_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct mp2629_charger *charger = dev_get_drvdata(psy->dev.parent); + unsigned int rval; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = regmap_read(charger->regmap, MP2629_REG_STATUS, &rval); + if (ret) + break; + + val->intval = !!(rval & MP2629_MASK_INPUT_TYPE); + break; + + case POWER_SUPPLY_PROP_USB_TYPE: + ret = regmap_read(charger->regmap, MP2629_REG_STATUS, &rval); + if (ret) + break; + + rval = (rval & MP2629_MASK_INPUT_TYPE) >> 5; + switch (rval) { + case MP2629_SOURCE_TYPE_SDP: + val->intval = POWER_SUPPLY_USB_TYPE_SDP; + break; + case MP2629_SOURCE_TYPE_CDP: + val->intval = POWER_SUPPLY_USB_TYPE_CDP; + break; + case MP2629_SOURCE_TYPE_DCP: + val->intval = POWER_SUPPLY_USB_TYPE_DCP; + break; + case MP2629_SOURCE_TYPE_OTG: + val->intval = POWER_SUPPLY_USB_TYPE_PD_DRP; + break; + default: + val->intval = POWER_SUPPLY_USB_TYPE_UNKNOWN; + break; + } + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = mp2629_read_adc(charger, MP2629_INPUT_VOLT, val); + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = mp2629_read_adc(charger, MP2629_INPUT_CURRENT, val); + break; + + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + ret = mp2629_get_prop(charger, INPUT_VLIM, val); + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = mp2629_get_prop(charger, INPUT_ILIM, val); + break; + + default: + return -EINVAL; + } + + return ret; +} + +static int mp2629_charger_usb_set_prop(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct mp2629_charger *charger = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT: + return mp2629_set_prop(charger, INPUT_VLIM, val); + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return mp2629_set_prop(charger, INPUT_ILIM, val); + + default: + return -EINVAL; + } +} + +static int mp2629_charger_battery_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return (psp == POWER_SUPPLY_PROP_PRECHARGE_CURRENT) || + (psp == POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT) || + (psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT) || + (psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE); +} + +static int mp2629_charger_usb_prop_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return (psp == POWER_SUPPLY_PROP_INPUT_VOLTAGE_LIMIT) || + (psp == POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT); +} + +static irqreturn_t mp2629_irq_handler(int irq, void *dev_id) +{ + struct mp2629_charger *charger = dev_id; + unsigned int rval; + int ret; + + mutex_lock(&charger->lock); + + ret = regmap_read(charger->regmap, MP2629_REG_FAULT, &rval); + if (ret) + goto unlock; + + if (rval) { + charger->fault = rval; + if (MP2629_FAULT_BATTERY & rval) + dev_err(charger->dev, "Battery fault OVP\n"); + else if (MP2629_FAULT_THERMAL & rval) + dev_err(charger->dev, "Thermal shutdown fault\n"); + else if (MP2629_FAULT_INPUT & rval) + dev_err(charger->dev, "no input or input OVP\n"); + else if (MP2629_FAULT_OTG & rval) + dev_err(charger->dev, "VIN overloaded\n"); + + goto unlock; + } + + ret = regmap_read(charger->regmap, MP2629_REG_STATUS, &rval); + if (ret) + goto unlock; + + if (rval & MP2629_INPUTSOURCE_CHANGE) + power_supply_changed(charger->usb); + else if (rval & MP2629_CHARGING_CHANGE) + power_supply_changed(charger->battery); + +unlock: + mutex_unlock(&charger->lock); + + return IRQ_HANDLED; +} + +static const struct power_supply_desc mp2629_usb_desc = { + .name = "mp2629_usb", + .type = POWER_SUPPLY_TYPE_USB, + .usb_types = mp2629_usb_types, + .num_usb_types = ARRAY_SIZE(mp2629_usb_types), + .properties = mp2629_charger_usb_props, + .num_properties = ARRAY_SIZE(mp2629_charger_usb_props), + .get_property = mp2629_charger_usb_get_prop, + .set_property = mp2629_charger_usb_set_prop, + .property_is_writeable = mp2629_charger_usb_prop_writeable, +}; + +static const struct power_supply_desc mp2629_battery_desc = { + .name = "mp2629_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = mp2629_charger_bat_props, + .num_properties = ARRAY_SIZE(mp2629_charger_bat_props), + .get_property = mp2629_charger_battery_get_prop, + .set_property = mp2629_charger_battery_set_prop, + .property_is_writeable = mp2629_charger_battery_prop_writeable, +}; + +static ssize_t batt_impedance_compensation_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct mp2629_charger *charger = dev_get_drvdata(dev->parent); + unsigned int rval; + int ret; + + ret = regmap_read(charger->regmap, MP2629_REG_IMPEDANCE_COMP, &rval); + if (ret) + return ret; + + rval = (rval >> 4) * 10; + return sprintf(buf, "%d mohm\n", rval); +} + +static ssize_t batt_impedance_compensation_store(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct mp2629_charger *charger = dev_get_drvdata(dev->parent); + unsigned int val; + int ret; + + ret = kstrtouint(buf, 10, &val); + if (ret) + return ret; + + if (val > 140) + return -ERANGE; + + /* multiples of 10 mohm so round off */ + val = val / 10; + ret = regmap_update_bits(charger->regmap, MP2629_REG_IMPEDANCE_COMP, + MP2629_MASK_IMPEDANCE, val << 4); + if (ret) + return ret; + + return count; +} + +static DEVICE_ATTR_RW(batt_impedance_compensation); + +static struct attribute *mp2629_charger_sysfs_attrs[] = { + &dev_attr_batt_impedance_compensation.attr, + NULL +}; +ATTRIBUTE_GROUPS(mp2629_charger_sysfs); + +static void mp2629_charger_disable(void *data) +{ + struct mp2629_charger *charger = data; + + regmap_update_bits(charger->regmap, MP2629_REG_CHARGE_CTRL, + MP2629_MASK_CHARGE_CTRL, 0); +} + +static int mp2629_charger_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct mp2629_data *ddata = dev_get_drvdata(dev->parent); + struct mp2629_charger *charger; + struct power_supply_config psy_cfg = {}; + int ret, i, irq; + + charger = devm_kzalloc(dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + charger->regmap = ddata->regmap; + charger->dev = dev; + platform_set_drvdata(pdev, charger); + + irq = platform_get_irq_optional(to_platform_device(dev->parent), 0); + if (irq < 0) { + dev_err(dev, "get irq fail: %d\n", irq); + return irq; + } + + for (i = 0; i < MP2629_MAX_FIELD; i++) { + charger->regmap_fields[i] = devm_regmap_field_alloc(dev, + charger->regmap, mp2629_reg_fields[i]); + if (IS_ERR(charger->regmap_fields[i])) { + dev_err(dev, "regmap field alloc fail %d\n", i); + return PTR_ERR(charger->regmap_fields[i]); + } + } + + for (i = 0; i < MP2629_ADC_CHAN_END; i++) { + charger->iiochan[i] = devm_iio_channel_get(dev, + adc_chan_name[i]); + if (IS_ERR(charger->iiochan[i])) { + dev_err(dev, "iio chan get %s err\n", adc_chan_name[i]); + return PTR_ERR(charger->iiochan[i]); + } + } + + ret = devm_add_action_or_reset(dev, mp2629_charger_disable, charger); + if (ret) + return ret; + + charger->usb = devm_power_supply_register(dev, &mp2629_usb_desc, NULL); + if (IS_ERR(charger->usb)) { + dev_err(dev, "power supply register usb failed\n"); + return PTR_ERR(charger->usb); + } + + psy_cfg.drv_data = charger; + psy_cfg.attr_grp = mp2629_charger_sysfs_groups; + charger->battery = devm_power_supply_register(dev, + &mp2629_battery_desc, &psy_cfg); + if (IS_ERR(charger->battery)) { + dev_err(dev, "power supply register battery failed\n"); + return PTR_ERR(charger->battery); + } + + ret = regmap_update_bits(charger->regmap, MP2629_REG_CHARGE_CTRL, + MP2629_MASK_CHARGE_CTRL, BIT(4)); + if (ret) { + dev_err(dev, "enable charge fail: %d\n", ret); + return ret; + } + + regmap_update_bits(charger->regmap, MP2629_REG_TIMER_CTRL, + MP2629_MASK_WDOG_CTRL, 0); + + mutex_init(&charger->lock); + + ret = devm_request_threaded_irq(dev, irq, NULL, mp2629_irq_handler, + IRQF_ONESHOT | IRQF_TRIGGER_RISING, + "mp2629-charger", charger); + if (ret) { + dev_err(dev, "failed to request gpio IRQ\n"); + return ret; + } + + regmap_update_bits(charger->regmap, MP2629_REG_INTERRUPT, + GENMASK(6, 5), BIT(6) | BIT(5)); + + return 0; +} + +static const struct of_device_id mp2629_charger_of_match[] = { + { .compatible = "mps,mp2629_charger"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2629_charger_of_match); + +static struct platform_driver mp2629_charger_driver = { + .driver = { + .name = "mp2629_charger", + .of_match_table = mp2629_charger_of_match, + }, + .probe = mp2629_charger_probe, +}; +module_platform_driver(mp2629_charger_driver); + +MODULE_AUTHOR("Saravanan Sekar <sravanhome@gmail.com>"); +MODULE_DESCRIPTION("MP2629 Charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/olpc_battery.c b/drivers/power/supply/olpc_battery.c new file mode 100644 index 000000000..e0476ec06 --- /dev/null +++ b/drivers/power/supply/olpc_battery.c @@ -0,0 +1,730 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for One Laptop Per Child board. + * + * Copyright © 2006-2010 David Woodhouse <dwmw2@infradead.org> + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/types.h> +#include <linux/err.h> +#include <linux/device.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/jiffies.h> +#include <linux/sched.h> +#include <linux/olpc-ec.h> + + +#define EC_BAT_VOLTAGE 0x10 /* uint16_t, *9.76/32, mV */ +#define EC_BAT_CURRENT 0x11 /* int16_t, *15.625/120, mA */ +#define EC_BAT_ACR 0x12 /* int16_t, *6250/15, µAh */ +#define EC_BAT_TEMP 0x13 /* uint16_t, *100/256, °C */ +#define EC_AMB_TEMP 0x14 /* uint16_t, *100/256, °C */ +#define EC_BAT_STATUS 0x15 /* uint8_t, bitmask */ +#define EC_BAT_SOC 0x16 /* uint8_t, percentage */ +#define EC_BAT_SERIAL 0x17 /* uint8_t[6] */ +#define EC_BAT_EEPROM 0x18 /* uint8_t adr as input, uint8_t output */ +#define EC_BAT_ERRCODE 0x1f /* uint8_t, bitmask */ + +#define BAT_STAT_PRESENT 0x01 +#define BAT_STAT_FULL 0x02 +#define BAT_STAT_LOW 0x04 +#define BAT_STAT_DESTROY 0x08 +#define BAT_STAT_AC 0x10 +#define BAT_STAT_CHARGING 0x20 +#define BAT_STAT_DISCHARGING 0x40 +#define BAT_STAT_TRICKLE 0x80 + +#define BAT_ERR_INFOFAIL 0x02 +#define BAT_ERR_OVERVOLTAGE 0x04 +#define BAT_ERR_OVERTEMP 0x05 +#define BAT_ERR_GAUGESTOP 0x06 +#define BAT_ERR_OUT_OF_CONTROL 0x07 +#define BAT_ERR_ID_FAIL 0x09 +#define BAT_ERR_ACR_FAIL 0x10 + +#define BAT_ADDR_MFR_TYPE 0x5F + +struct olpc_battery_data { + struct power_supply *olpc_ac; + struct power_supply *olpc_bat; + char bat_serial[17]; + bool new_proto; + bool little_endian; +}; + +/********************************************************************* + * Power + *********************************************************************/ + +static int olpc_ac_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + uint8_t status; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = olpc_ec_cmd(EC_BAT_STATUS, NULL, 0, &status, 1); + if (ret) + return ret; + + val->intval = !!(status & BAT_STAT_AC); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static enum power_supply_property olpc_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc olpc_ac_desc = { + .name = "olpc_ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = olpc_ac_props, + .num_properties = ARRAY_SIZE(olpc_ac_props), + .get_property = olpc_ac_get_prop, +}; + +static int olpc_bat_get_status(struct olpc_battery_data *data, + union power_supply_propval *val, uint8_t ec_byte) +{ + if (data->new_proto) { + if (ec_byte & (BAT_STAT_CHARGING | BAT_STAT_TRICKLE)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (ec_byte & BAT_STAT_DISCHARGING) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (ec_byte & BAT_STAT_FULL) + val->intval = POWER_SUPPLY_STATUS_FULL; + else /* er,... */ + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + /* Older EC didn't report charge/discharge bits */ + if (!(ec_byte & BAT_STAT_AC)) /* No AC means discharging */ + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (ec_byte & BAT_STAT_FULL) + val->intval = POWER_SUPPLY_STATUS_FULL; + else /* Not _necessarily_ true but EC doesn't tell all yet */ + val->intval = POWER_SUPPLY_STATUS_CHARGING; + } + + return 0; +} + +static int olpc_bat_get_health(union power_supply_propval *val) +{ + uint8_t ec_byte; + int ret; + + ret = olpc_ec_cmd(EC_BAT_ERRCODE, NULL, 0, &ec_byte, 1); + if (ret) + return ret; + + switch (ec_byte) { + case 0: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + + case BAT_ERR_OVERTEMP: + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + + case BAT_ERR_OVERVOLTAGE: + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + break; + + case BAT_ERR_INFOFAIL: + case BAT_ERR_OUT_OF_CONTROL: + case BAT_ERR_ID_FAIL: + case BAT_ERR_ACR_FAIL: + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + + default: + /* Eep. We don't know this failure code */ + ret = -EIO; + } + + return ret; +} + +static int olpc_bat_get_mfr(union power_supply_propval *val) +{ + uint8_t ec_byte; + int ret; + + ec_byte = BAT_ADDR_MFR_TYPE; + ret = olpc_ec_cmd(EC_BAT_EEPROM, &ec_byte, 1, &ec_byte, 1); + if (ret) + return ret; + + switch (ec_byte >> 4) { + case 1: + val->strval = "Gold Peak"; + break; + case 2: + val->strval = "BYD"; + break; + default: + val->strval = "Unknown"; + break; + } + + return ret; +} + +static int olpc_bat_get_tech(union power_supply_propval *val) +{ + uint8_t ec_byte; + int ret; + + ec_byte = BAT_ADDR_MFR_TYPE; + ret = olpc_ec_cmd(EC_BAT_EEPROM, &ec_byte, 1, &ec_byte, 1); + if (ret) + return ret; + + switch (ec_byte & 0xf) { + case 1: + val->intval = POWER_SUPPLY_TECHNOLOGY_NiMH; + break; + case 2: + val->intval = POWER_SUPPLY_TECHNOLOGY_LiFe; + break; + default: + val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + break; + } + + return ret; +} + +static int olpc_bat_get_charge_full_design(union power_supply_propval *val) +{ + uint8_t ec_byte; + union power_supply_propval tech; + int ret, mfr; + + ret = olpc_bat_get_tech(&tech); + if (ret) + return ret; + + ec_byte = BAT_ADDR_MFR_TYPE; + ret = olpc_ec_cmd(EC_BAT_EEPROM, &ec_byte, 1, &ec_byte, 1); + if (ret) + return ret; + + mfr = ec_byte >> 4; + + switch (tech.intval) { + case POWER_SUPPLY_TECHNOLOGY_NiMH: + switch (mfr) { + case 1: /* Gold Peak */ + val->intval = 3000000*.8; + break; + default: + return -EIO; + } + break; + + case POWER_SUPPLY_TECHNOLOGY_LiFe: + switch (mfr) { + case 1: /* Gold Peak, fall through */ + case 2: /* BYD */ + val->intval = 2800000; + break; + default: + return -EIO; + } + break; + + default: + return -EIO; + } + + return ret; +} + +static int olpc_bat_get_charge_now(union power_supply_propval *val) +{ + uint8_t soc; + union power_supply_propval full; + int ret; + + ret = olpc_ec_cmd(EC_BAT_SOC, NULL, 0, &soc, 1); + if (ret) + return ret; + + ret = olpc_bat_get_charge_full_design(&full); + if (ret) + return ret; + + val->intval = soc * (full.intval / 100); + return 0; +} + +static int olpc_bat_get_voltage_max_design(union power_supply_propval *val) +{ + uint8_t ec_byte; + union power_supply_propval tech; + int mfr; + int ret; + + ret = olpc_bat_get_tech(&tech); + if (ret) + return ret; + + ec_byte = BAT_ADDR_MFR_TYPE; + ret = olpc_ec_cmd(EC_BAT_EEPROM, &ec_byte, 1, &ec_byte, 1); + if (ret) + return ret; + + mfr = ec_byte >> 4; + + switch (tech.intval) { + case POWER_SUPPLY_TECHNOLOGY_NiMH: + switch (mfr) { + case 1: /* Gold Peak */ + val->intval = 6000000; + break; + default: + return -EIO; + } + break; + + case POWER_SUPPLY_TECHNOLOGY_LiFe: + switch (mfr) { + case 1: /* Gold Peak */ + val->intval = 6400000; + break; + case 2: /* BYD */ + val->intval = 6500000; + break; + default: + return -EIO; + } + break; + + default: + return -EIO; + } + + return ret; +} + +static u16 ecword_to_cpu(struct olpc_battery_data *data, u16 ec_word) +{ + if (data->little_endian) + return le16_to_cpu((__force __le16)ec_word); + else + return be16_to_cpu((__force __be16)ec_word); +} + +/********************************************************************* + * Battery properties + *********************************************************************/ +static int olpc_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct olpc_battery_data *data = power_supply_get_drvdata(psy); + int ret = 0; + u16 ec_word; + uint8_t ec_byte; + __be64 ser_buf; + + ret = olpc_ec_cmd(EC_BAT_STATUS, NULL, 0, &ec_byte, 1); + if (ret) + return ret; + + /* Theoretically there's a race here -- the battery could be + removed immediately after we check whether it's present, and + then we query for some other property of the now-absent battery. + It doesn't matter though -- the EC will return the last-known + information, and it's as if we just ran that _little_ bit faster + and managed to read it out before the battery went away. */ + if (!(ec_byte & (BAT_STAT_PRESENT | BAT_STAT_TRICKLE)) && + psp != POWER_SUPPLY_PROP_PRESENT) + return -ENODEV; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = olpc_bat_get_status(data, val, ec_byte); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + if (ec_byte & BAT_STAT_TRICKLE) + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + else if (ec_byte & BAT_STAT_CHARGING) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(ec_byte & (BAT_STAT_PRESENT | + BAT_STAT_TRICKLE)); + break; + + case POWER_SUPPLY_PROP_HEALTH: + if (ec_byte & BAT_STAT_DESTROY) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else { + ret = olpc_bat_get_health(val); + if (ret) + return ret; + } + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + ret = olpc_bat_get_mfr(val); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + ret = olpc_bat_get_tech(val); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = olpc_ec_cmd(EC_BAT_VOLTAGE, NULL, 0, (void *)&ec_word, 2); + if (ret) + return ret; + + val->intval = ecword_to_cpu(data, ec_word) * 9760L / 32; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = olpc_ec_cmd(EC_BAT_CURRENT, NULL, 0, (void *)&ec_word, 2); + if (ret) + return ret; + + val->intval = ecword_to_cpu(data, ec_word) * 15625L / 120; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = olpc_ec_cmd(EC_BAT_SOC, NULL, 0, &ec_byte, 1); + if (ret) + return ret; + val->intval = ec_byte; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + if (ec_byte & BAT_STAT_FULL) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (ec_byte & BAT_STAT_LOW) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = olpc_bat_get_charge_full_design(val); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = olpc_bat_get_charge_now(val); + if (ret) + return ret; + break; + case POWER_SUPPLY_PROP_TEMP: + ret = olpc_ec_cmd(EC_BAT_TEMP, NULL, 0, (void *)&ec_word, 2); + if (ret) + return ret; + + val->intval = ecword_to_cpu(data, ec_word) * 10 / 256; + break; + case POWER_SUPPLY_PROP_TEMP_AMBIENT: + ret = olpc_ec_cmd(EC_AMB_TEMP, NULL, 0, (void *)&ec_word, 2); + if (ret) + return ret; + + val->intval = (int)ecword_to_cpu(data, ec_word) * 10 / 256; + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + ret = olpc_ec_cmd(EC_BAT_ACR, NULL, 0, (void *)&ec_word, 2); + if (ret) + return ret; + + val->intval = ecword_to_cpu(data, ec_word) * 6250 / 15; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + ret = olpc_ec_cmd(EC_BAT_SERIAL, NULL, 0, (void *)&ser_buf, 8); + if (ret) + return ret; + + sprintf(data->bat_serial, "%016llx", (long long)be64_to_cpu(ser_buf)); + val->strval = data->bat_serial; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + ret = olpc_bat_get_voltage_max_design(val); + if (ret) + return ret; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property olpc_xo1_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TEMP_AMBIENT, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, +}; + +/* XO-1.5 does not have ambient temperature property */ +static enum power_supply_property olpc_xo15_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, +}; + +/* EEPROM reading goes completely around the power_supply API, sadly */ + +#define EEPROM_START 0x20 +#define EEPROM_END 0x80 +#define EEPROM_SIZE (EEPROM_END - EEPROM_START) + +static ssize_t olpc_bat_eeprom_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *attr, char *buf, loff_t off, size_t count) +{ + uint8_t ec_byte; + int ret; + int i; + + for (i = 0; i < count; i++) { + ec_byte = EEPROM_START + off + i; + ret = olpc_ec_cmd(EC_BAT_EEPROM, &ec_byte, 1, &buf[i], 1); + if (ret) { + pr_err("olpc-battery: " + "EC_BAT_EEPROM cmd @ 0x%x failed - %d!\n", + ec_byte, ret); + return -EIO; + } + } + + return count; +} + +static struct bin_attribute olpc_bat_eeprom = { + .attr = { + .name = "eeprom", + .mode = S_IRUGO, + }, + .size = EEPROM_SIZE, + .read = olpc_bat_eeprom_read, +}; + +/* Allow userspace to see the specific error value pulled from the EC */ + +static ssize_t olpc_bat_error_read(struct device *dev, + struct device_attribute *attr, char *buf) +{ + uint8_t ec_byte; + ssize_t ret; + + ret = olpc_ec_cmd(EC_BAT_ERRCODE, NULL, 0, &ec_byte, 1); + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", ec_byte); +} + +static struct device_attribute olpc_bat_error = { + .attr = { + .name = "error", + .mode = S_IRUGO, + }, + .show = olpc_bat_error_read, +}; + +static struct attribute *olpc_bat_sysfs_attrs[] = { + &olpc_bat_error.attr, + NULL +}; + +static struct bin_attribute *olpc_bat_sysfs_bin_attrs[] = { + &olpc_bat_eeprom, + NULL +}; + +static const struct attribute_group olpc_bat_sysfs_group = { + .attrs = olpc_bat_sysfs_attrs, + .bin_attrs = olpc_bat_sysfs_bin_attrs, + +}; + +static const struct attribute_group *olpc_bat_sysfs_groups[] = { + &olpc_bat_sysfs_group, + NULL +}; + +/********************************************************************* + * Initialisation + *********************************************************************/ + +static struct power_supply_desc olpc_bat_desc = { + .name = "olpc_battery", + .get_property = olpc_bat_get_property, + .use_for_apm = 1, +}; + +static int olpc_battery_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct olpc_battery_data *data = platform_get_drvdata(pdev); + + if (device_may_wakeup(&data->olpc_ac->dev)) + olpc_ec_wakeup_set(EC_SCI_SRC_ACPWR); + else + olpc_ec_wakeup_clear(EC_SCI_SRC_ACPWR); + + if (device_may_wakeup(&data->olpc_bat->dev)) + olpc_ec_wakeup_set(EC_SCI_SRC_BATTERY | EC_SCI_SRC_BATSOC + | EC_SCI_SRC_BATERR); + else + olpc_ec_wakeup_clear(EC_SCI_SRC_BATTERY | EC_SCI_SRC_BATSOC + | EC_SCI_SRC_BATERR); + + return 0; +} + +static int olpc_battery_probe(struct platform_device *pdev) +{ + struct power_supply_config bat_psy_cfg = {}; + struct power_supply_config ac_psy_cfg = {}; + struct olpc_battery_data *data; + uint8_t status; + uint8_t ecver; + int ret; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + platform_set_drvdata(pdev, data); + + /* See if the EC is already there and get the EC revision */ + ret = olpc_ec_cmd(EC_FIRMWARE_REV, NULL, 0, &ecver, 1); + if (ret) + return ret; + + if (of_find_compatible_node(NULL, NULL, "olpc,xo1.75-ec")) { + /* XO 1.75 */ + data->new_proto = true; + data->little_endian = true; + } else if (ecver > 0x44) { + /* XO 1 or 1.5 with a new EC firmware. */ + data->new_proto = true; + } else if (ecver < 0x44) { + /* + * We've seen a number of EC protocol changes; this driver + * requires the latest EC protocol, supported by 0x44 and above. + */ + printk(KERN_NOTICE "OLPC EC version 0x%02x too old for " + "battery driver.\n", ecver); + return -ENXIO; + } + + ret = olpc_ec_cmd(EC_BAT_STATUS, NULL, 0, &status, 1); + if (ret) + return ret; + + /* Ignore the status. It doesn't actually matter */ + + ac_psy_cfg.of_node = pdev->dev.of_node; + ac_psy_cfg.drv_data = data; + + data->olpc_ac = devm_power_supply_register(&pdev->dev, &olpc_ac_desc, + &ac_psy_cfg); + if (IS_ERR(data->olpc_ac)) + return PTR_ERR(data->olpc_ac); + + if (of_device_is_compatible(pdev->dev.of_node, "olpc,xo1.5-battery")) { + /* XO-1.5 */ + olpc_bat_desc.properties = olpc_xo15_bat_props; + olpc_bat_desc.num_properties = ARRAY_SIZE(olpc_xo15_bat_props); + } else { + /* XO-1 */ + olpc_bat_desc.properties = olpc_xo1_bat_props; + olpc_bat_desc.num_properties = ARRAY_SIZE(olpc_xo1_bat_props); + } + + bat_psy_cfg.of_node = pdev->dev.of_node; + bat_psy_cfg.drv_data = data; + bat_psy_cfg.attr_grp = olpc_bat_sysfs_groups; + + data->olpc_bat = devm_power_supply_register(&pdev->dev, &olpc_bat_desc, + &bat_psy_cfg); + if (IS_ERR(data->olpc_bat)) + return PTR_ERR(data->olpc_bat); + + if (olpc_ec_wakeup_available()) { + device_set_wakeup_capable(&data->olpc_ac->dev, true); + device_set_wakeup_capable(&data->olpc_bat->dev, true); + } + + return 0; +} + +static const struct of_device_id olpc_battery_ids[] = { + { .compatible = "olpc,xo1-battery" }, + { .compatible = "olpc,xo1.5-battery" }, + {} +}; +MODULE_DEVICE_TABLE(of, olpc_battery_ids); + +static struct platform_driver olpc_battery_driver = { + .driver = { + .name = "olpc-battery", + .of_match_table = olpc_battery_ids, + }, + .probe = olpc_battery_probe, + .suspend = olpc_battery_suspend, +}; + +module_platform_driver(olpc_battery_driver); + +MODULE_AUTHOR("David Woodhouse <dwmw2@infradead.org>"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Battery driver for One Laptop Per Child 'XO' machine"); diff --git a/drivers/power/supply/pcf50633-charger.c b/drivers/power/supply/pcf50633-charger.c new file mode 100644 index 000000000..8c5d892f6 --- /dev/null +++ b/drivers/power/supply/pcf50633-charger.c @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* NXP PCF50633 Main Battery Charger Driver + * + * (C) 2006-2008 by Openmoko, Inc. + * Author: Balaji Rao <balajirrao@openmoko.org> + * All rights reserved. + * + * Broken down from monstrous PCF50633 driver mainly by + * Harald Welte, Andy Green and Werner Almesberger + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/device.h> +#include <linux/sysfs.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> + +#include <linux/mfd/pcf50633/core.h> +#include <linux/mfd/pcf50633/mbc.h> + +struct pcf50633_mbc { + struct pcf50633 *pcf; + + int adapter_online; + int usb_online; + + struct power_supply *usb; + struct power_supply *adapter; + struct power_supply *ac; +}; + +int pcf50633_mbc_usb_curlim_set(struct pcf50633 *pcf, int ma) +{ + struct pcf50633_mbc *mbc = platform_get_drvdata(pcf->mbc_pdev); + int ret = 0; + u8 bits; + u8 mbcs2, chgmod; + unsigned int mbcc5; + + if (ma >= 1000) { + bits = PCF50633_MBCC7_USB_1000mA; + ma = 1000; + } else if (ma >= 500) { + bits = PCF50633_MBCC7_USB_500mA; + ma = 500; + } else if (ma >= 100) { + bits = PCF50633_MBCC7_USB_100mA; + ma = 100; + } else { + bits = PCF50633_MBCC7_USB_SUSPEND; + ma = 0; + } + + ret = pcf50633_reg_set_bit_mask(pcf, PCF50633_REG_MBCC7, + PCF50633_MBCC7_USB_MASK, bits); + if (ret) + dev_err(pcf->dev, "error setting usb curlim to %d mA\n", ma); + else + dev_info(pcf->dev, "usb curlim to %d mA\n", ma); + + /* + * We limit the charging current to be the USB current limit. + * The reason is that on pcf50633, when it enters PMU Standby mode, + * which it does when the device goes "off", the USB current limit + * reverts to the variant default. In at least one common case, that + * default is 500mA. By setting the charging current to be the same + * as the USB limit we set here before PMU standby, we enforce it only + * using the correct amount of current even when the USB current limit + * gets reset to the wrong thing + */ + + if (mbc->pcf->pdata->charger_reference_current_ma) { + mbcc5 = (ma << 8) / mbc->pcf->pdata->charger_reference_current_ma; + if (mbcc5 > 255) + mbcc5 = 255; + pcf50633_reg_write(mbc->pcf, PCF50633_REG_MBCC5, mbcc5); + } + + mbcs2 = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCS2); + chgmod = (mbcs2 & PCF50633_MBCS2_MBC_MASK); + + /* If chgmod == BATFULL, setting chgena has no effect. + * Datasheet says we need to set resume instead but when autoresume is + * used resume doesn't work. Clear and set chgena instead. + */ + if (chgmod != PCF50633_MBCS2_MBC_BAT_FULL) + pcf50633_reg_set_bit_mask(pcf, PCF50633_REG_MBCC1, + PCF50633_MBCC1_CHGENA, PCF50633_MBCC1_CHGENA); + else { + pcf50633_reg_clear_bits(pcf, PCF50633_REG_MBCC1, + PCF50633_MBCC1_CHGENA); + pcf50633_reg_set_bit_mask(pcf, PCF50633_REG_MBCC1, + PCF50633_MBCC1_CHGENA, PCF50633_MBCC1_CHGENA); + } + + power_supply_changed(mbc->usb); + + return ret; +} +EXPORT_SYMBOL_GPL(pcf50633_mbc_usb_curlim_set); + +int pcf50633_mbc_get_status(struct pcf50633 *pcf) +{ + struct pcf50633_mbc *mbc = platform_get_drvdata(pcf->mbc_pdev); + int status = 0; + u8 chgmod; + + if (!mbc) + return 0; + + chgmod = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCS2) + & PCF50633_MBCS2_MBC_MASK; + + if (mbc->usb_online) + status |= PCF50633_MBC_USB_ONLINE; + if (chgmod == PCF50633_MBCS2_MBC_USB_PRE || + chgmod == PCF50633_MBCS2_MBC_USB_PRE_WAIT || + chgmod == PCF50633_MBCS2_MBC_USB_FAST || + chgmod == PCF50633_MBCS2_MBC_USB_FAST_WAIT) + status |= PCF50633_MBC_USB_ACTIVE; + if (mbc->adapter_online) + status |= PCF50633_MBC_ADAPTER_ONLINE; + if (chgmod == PCF50633_MBCS2_MBC_ADP_PRE || + chgmod == PCF50633_MBCS2_MBC_ADP_PRE_WAIT || + chgmod == PCF50633_MBCS2_MBC_ADP_FAST || + chgmod == PCF50633_MBCS2_MBC_ADP_FAST_WAIT) + status |= PCF50633_MBC_ADAPTER_ACTIVE; + + return status; +} +EXPORT_SYMBOL_GPL(pcf50633_mbc_get_status); + +int pcf50633_mbc_get_usb_online_status(struct pcf50633 *pcf) +{ + struct pcf50633_mbc *mbc = platform_get_drvdata(pcf->mbc_pdev); + + if (!mbc) + return 0; + + return mbc->usb_online; +} +EXPORT_SYMBOL_GPL(pcf50633_mbc_get_usb_online_status); + +static ssize_t +show_chgmode(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct pcf50633_mbc *mbc = dev_get_drvdata(dev); + + u8 mbcs2 = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCS2); + u8 chgmod = (mbcs2 & PCF50633_MBCS2_MBC_MASK); + + return sprintf(buf, "%d\n", chgmod); +} +static DEVICE_ATTR(chgmode, S_IRUGO, show_chgmode, NULL); + +static ssize_t +show_usblim(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct pcf50633_mbc *mbc = dev_get_drvdata(dev); + u8 usblim = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCC7) & + PCF50633_MBCC7_USB_MASK; + unsigned int ma; + + if (usblim == PCF50633_MBCC7_USB_1000mA) + ma = 1000; + else if (usblim == PCF50633_MBCC7_USB_500mA) + ma = 500; + else if (usblim == PCF50633_MBCC7_USB_100mA) + ma = 100; + else + ma = 0; + + return sprintf(buf, "%u\n", ma); +} + +static ssize_t set_usblim(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct pcf50633_mbc *mbc = dev_get_drvdata(dev); + unsigned long ma; + int ret; + + ret = kstrtoul(buf, 10, &ma); + if (ret) + return ret; + + pcf50633_mbc_usb_curlim_set(mbc->pcf, ma); + + return count; +} + +static DEVICE_ATTR(usb_curlim, S_IRUGO | S_IWUSR, show_usblim, set_usblim); + +static ssize_t +show_chglim(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct pcf50633_mbc *mbc = dev_get_drvdata(dev); + u8 mbcc5 = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCC5); + unsigned int ma; + + if (!mbc->pcf->pdata->charger_reference_current_ma) + return -ENODEV; + + ma = (mbc->pcf->pdata->charger_reference_current_ma * mbcc5) >> 8; + + return sprintf(buf, "%u\n", ma); +} + +static ssize_t set_chglim(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct pcf50633_mbc *mbc = dev_get_drvdata(dev); + unsigned long ma; + unsigned int mbcc5; + int ret; + + if (!mbc->pcf->pdata->charger_reference_current_ma) + return -ENODEV; + + ret = kstrtoul(buf, 10, &ma); + if (ret) + return ret; + + mbcc5 = (ma << 8) / mbc->pcf->pdata->charger_reference_current_ma; + if (mbcc5 > 255) + mbcc5 = 255; + pcf50633_reg_write(mbc->pcf, PCF50633_REG_MBCC5, mbcc5); + + return count; +} + +/* + * This attribute allows to change MBC charging limit on the fly + * independently of usb current limit. It also gets set automatically every + * time usb current limit is changed. + */ +static DEVICE_ATTR(chg_curlim, S_IRUGO | S_IWUSR, show_chglim, set_chglim); + +static struct attribute *pcf50633_mbc_sysfs_attrs[] = { + &dev_attr_chgmode.attr, + &dev_attr_usb_curlim.attr, + &dev_attr_chg_curlim.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(pcf50633_mbc_sysfs); + +static void +pcf50633_mbc_irq_handler(int irq, void *data) +{ + struct pcf50633_mbc *mbc = data; + + /* USB */ + if (irq == PCF50633_IRQ_USBINS) { + mbc->usb_online = 1; + } else if (irq == PCF50633_IRQ_USBREM) { + mbc->usb_online = 0; + pcf50633_mbc_usb_curlim_set(mbc->pcf, 0); + } + + /* Adapter */ + if (irq == PCF50633_IRQ_ADPINS) + mbc->adapter_online = 1; + else if (irq == PCF50633_IRQ_ADPREM) + mbc->adapter_online = 0; + + power_supply_changed(mbc->ac); + power_supply_changed(mbc->usb); + power_supply_changed(mbc->adapter); + + if (mbc->pcf->pdata->mbc_event_callback) + mbc->pcf->pdata->mbc_event_callback(mbc->pcf, irq); +} + +static int adapter_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pcf50633_mbc *mbc = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = mbc->adapter_online; + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static int usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pcf50633_mbc *mbc = power_supply_get_drvdata(psy); + int ret = 0; + u8 usblim = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCC7) & + PCF50633_MBCC7_USB_MASK; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = mbc->usb_online && + (usblim <= PCF50633_MBCC7_USB_500mA); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static int ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pcf50633_mbc *mbc = power_supply_get_drvdata(psy); + int ret = 0; + u8 usblim = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCC7) & + PCF50633_MBCC7_USB_MASK; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = mbc->usb_online && + (usblim == PCF50633_MBCC7_USB_1000mA); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static enum power_supply_property power_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const u8 mbc_irq_handlers[] = { + PCF50633_IRQ_ADPINS, + PCF50633_IRQ_ADPREM, + PCF50633_IRQ_USBINS, + PCF50633_IRQ_USBREM, + PCF50633_IRQ_BATFULL, + PCF50633_IRQ_CHGHALT, + PCF50633_IRQ_THLIMON, + PCF50633_IRQ_THLIMOFF, + PCF50633_IRQ_USBLIMON, + PCF50633_IRQ_USBLIMOFF, + PCF50633_IRQ_LOWSYS, + PCF50633_IRQ_LOWBAT, +}; + +static const struct power_supply_desc pcf50633_mbc_adapter_desc = { + .name = "adapter", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = power_props, + .num_properties = ARRAY_SIZE(power_props), + .get_property = &adapter_get_property, +}; + +static const struct power_supply_desc pcf50633_mbc_usb_desc = { + .name = "usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = power_props, + .num_properties = ARRAY_SIZE(power_props), + .get_property = usb_get_property, +}; + +static const struct power_supply_desc pcf50633_mbc_ac_desc = { + .name = "ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = power_props, + .num_properties = ARRAY_SIZE(power_props), + .get_property = ac_get_property, +}; + +static int pcf50633_mbc_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + struct power_supply_config usb_psy_cfg; + struct pcf50633_mbc *mbc; + int i; + u8 mbcs1; + + mbc = devm_kzalloc(&pdev->dev, sizeof(*mbc), GFP_KERNEL); + if (!mbc) + return -ENOMEM; + + platform_set_drvdata(pdev, mbc); + mbc->pcf = dev_to_pcf50633(pdev->dev.parent); + + /* Set up IRQ handlers */ + for (i = 0; i < ARRAY_SIZE(mbc_irq_handlers); i++) + pcf50633_register_irq(mbc->pcf, mbc_irq_handlers[i], + pcf50633_mbc_irq_handler, mbc); + + psy_cfg.supplied_to = mbc->pcf->pdata->batteries; + psy_cfg.num_supplicants = mbc->pcf->pdata->num_batteries; + psy_cfg.drv_data = mbc; + + /* Create power supplies */ + mbc->adapter = power_supply_register(&pdev->dev, + &pcf50633_mbc_adapter_desc, + &psy_cfg); + if (IS_ERR(mbc->adapter)) { + dev_err(mbc->pcf->dev, "failed to register adapter\n"); + return PTR_ERR(mbc->adapter); + } + + usb_psy_cfg = psy_cfg; + usb_psy_cfg.attr_grp = pcf50633_mbc_sysfs_groups; + + mbc->usb = power_supply_register(&pdev->dev, &pcf50633_mbc_usb_desc, + &usb_psy_cfg); + if (IS_ERR(mbc->usb)) { + dev_err(mbc->pcf->dev, "failed to register usb\n"); + power_supply_unregister(mbc->adapter); + return PTR_ERR(mbc->usb); + } + + mbc->ac = power_supply_register(&pdev->dev, &pcf50633_mbc_ac_desc, + &psy_cfg); + if (IS_ERR(mbc->ac)) { + dev_err(mbc->pcf->dev, "failed to register ac\n"); + power_supply_unregister(mbc->adapter); + power_supply_unregister(mbc->usb); + return PTR_ERR(mbc->ac); + } + + mbcs1 = pcf50633_reg_read(mbc->pcf, PCF50633_REG_MBCS1); + if (mbcs1 & PCF50633_MBCS1_USBPRES) + pcf50633_mbc_irq_handler(PCF50633_IRQ_USBINS, mbc); + if (mbcs1 & PCF50633_MBCS1_ADAPTPRES) + pcf50633_mbc_irq_handler(PCF50633_IRQ_ADPINS, mbc); + + return 0; +} + +static int pcf50633_mbc_remove(struct platform_device *pdev) +{ + struct pcf50633_mbc *mbc = platform_get_drvdata(pdev); + int i; + + /* Remove IRQ handlers */ + for (i = 0; i < ARRAY_SIZE(mbc_irq_handlers); i++) + pcf50633_free_irq(mbc->pcf, mbc_irq_handlers[i]); + + power_supply_unregister(mbc->usb); + power_supply_unregister(mbc->adapter); + power_supply_unregister(mbc->ac); + + return 0; +} + +static struct platform_driver pcf50633_mbc_driver = { + .driver = { + .name = "pcf50633-mbc", + }, + .probe = pcf50633_mbc_probe, + .remove = pcf50633_mbc_remove, +}; + +module_platform_driver(pcf50633_mbc_driver); + +MODULE_AUTHOR("Balaji Rao <balajirrao@openmoko.org>"); +MODULE_DESCRIPTION("PCF50633 mbc driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:pcf50633-mbc"); diff --git a/drivers/power/supply/pda_power.c b/drivers/power/supply/pda_power.c new file mode 100644 index 000000000..03a37fd6b --- /dev/null +++ b/drivers/power/supply/pda_power.c @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Common power driver for PDAs and phones with one or two external + * power supplies (AC/USB) connected to main and backup batteries, + * and optional builtin charger. + * + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/notifier.h> +#include <linux/power_supply.h> +#include <linux/pda_power.h> +#include <linux/regulator/consumer.h> +#include <linux/timer.h> +#include <linux/jiffies.h> +#include <linux/usb/otg.h> + +static inline unsigned int get_irq_flags(struct resource *res) +{ + return IRQF_SHARED | (res->flags & IRQF_TRIGGER_MASK); +} + +static struct device *dev; +static struct pda_power_pdata *pdata; +static struct resource *ac_irq, *usb_irq; +static struct delayed_work charger_work; +static struct delayed_work polling_work; +static struct delayed_work supply_work; +static int polling; +static struct power_supply *pda_psy_ac, *pda_psy_usb; + +#if IS_ENABLED(CONFIG_USB_PHY) +static struct usb_phy *transceiver; +static struct notifier_block otg_nb; +#endif + +static struct regulator *ac_draw; + +enum { + PDA_PSY_OFFLINE = 0, + PDA_PSY_ONLINE = 1, + PDA_PSY_TO_CHANGE, +}; +static int new_ac_status = -1; +static int new_usb_status = -1; +static int ac_status = -1; +static int usb_status = -1; + +static int pda_power_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + if (psy->desc->type == POWER_SUPPLY_TYPE_MAINS) + val->intval = pdata->is_ac_online ? + pdata->is_ac_online() : 0; + else + val->intval = pdata->is_usb_online ? + pdata->is_usb_online() : 0; + break; + default: + return -EINVAL; + } + return 0; +} + +static enum power_supply_property pda_power_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static char *pda_power_supplied_to[] = { + "main-battery", + "backup-battery", +}; + +static const struct power_supply_desc pda_psy_ac_desc = { + .name = "ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = pda_power_props, + .num_properties = ARRAY_SIZE(pda_power_props), + .get_property = pda_power_get_property, +}; + +static const struct power_supply_desc pda_psy_usb_desc = { + .name = "usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = pda_power_props, + .num_properties = ARRAY_SIZE(pda_power_props), + .get_property = pda_power_get_property, +}; + +static void update_status(void) +{ + if (pdata->is_ac_online) + new_ac_status = !!pdata->is_ac_online(); + + if (pdata->is_usb_online) + new_usb_status = !!pdata->is_usb_online(); +} + +static void update_charger(void) +{ + static int regulator_enabled; + int max_uA = pdata->ac_max_uA; + + if (pdata->set_charge) { + if (new_ac_status > 0) { + dev_dbg(dev, "charger on (AC)\n"); + pdata->set_charge(PDA_POWER_CHARGE_AC); + } else if (new_usb_status > 0) { + dev_dbg(dev, "charger on (USB)\n"); + pdata->set_charge(PDA_POWER_CHARGE_USB); + } else { + dev_dbg(dev, "charger off\n"); + pdata->set_charge(0); + } + } else if (ac_draw) { + if (new_ac_status > 0) { + regulator_set_current_limit(ac_draw, max_uA, max_uA); + if (!regulator_enabled) { + dev_dbg(dev, "charger on (AC)\n"); + WARN_ON(regulator_enable(ac_draw)); + regulator_enabled = 1; + } + } else { + if (regulator_enabled) { + dev_dbg(dev, "charger off\n"); + WARN_ON(regulator_disable(ac_draw)); + regulator_enabled = 0; + } + } + } +} + +static void supply_work_func(struct work_struct *work) +{ + if (ac_status == PDA_PSY_TO_CHANGE) { + ac_status = new_ac_status; + power_supply_changed(pda_psy_ac); + } + + if (usb_status == PDA_PSY_TO_CHANGE) { + usb_status = new_usb_status; + power_supply_changed(pda_psy_usb); + } +} + +static void psy_changed(void) +{ + update_charger(); + + /* + * Okay, charger set. Now wait a bit before notifying supplicants, + * charge power should stabilize. + */ + cancel_delayed_work(&supply_work); + schedule_delayed_work(&supply_work, + msecs_to_jiffies(pdata->wait_for_charger)); +} + +static void charger_work_func(struct work_struct *work) +{ + update_status(); + psy_changed(); +} + +static irqreturn_t power_changed_isr(int irq, void *power_supply) +{ + if (power_supply == pda_psy_ac) + ac_status = PDA_PSY_TO_CHANGE; + else if (power_supply == pda_psy_usb) + usb_status = PDA_PSY_TO_CHANGE; + else + return IRQ_NONE; + + /* + * Wait a bit before reading ac/usb line status and setting charger, + * because ac/usb status readings may lag from irq. + */ + cancel_delayed_work(&charger_work); + schedule_delayed_work(&charger_work, + msecs_to_jiffies(pdata->wait_for_status)); + + return IRQ_HANDLED; +} + +static void polling_work_func(struct work_struct *work) +{ + int changed = 0; + + dev_dbg(dev, "polling...\n"); + + update_status(); + + if (!ac_irq && new_ac_status != ac_status) { + ac_status = PDA_PSY_TO_CHANGE; + changed = 1; + } + + if (!usb_irq && new_usb_status != usb_status) { + usb_status = PDA_PSY_TO_CHANGE; + changed = 1; + } + + if (changed) + psy_changed(); + + cancel_delayed_work(&polling_work); + schedule_delayed_work(&polling_work, + msecs_to_jiffies(pdata->polling_interval)); +} + +#if IS_ENABLED(CONFIG_USB_PHY) +static int otg_is_usb_online(void) +{ + return (transceiver->last_event == USB_EVENT_VBUS || + transceiver->last_event == USB_EVENT_ENUMERATED); +} + +static int otg_is_ac_online(void) +{ + return (transceiver->last_event == USB_EVENT_CHARGER); +} + +static int otg_handle_notification(struct notifier_block *nb, + unsigned long event, void *unused) +{ + switch (event) { + case USB_EVENT_CHARGER: + ac_status = PDA_PSY_TO_CHANGE; + break; + case USB_EVENT_VBUS: + case USB_EVENT_ENUMERATED: + usb_status = PDA_PSY_TO_CHANGE; + break; + case USB_EVENT_NONE: + ac_status = PDA_PSY_TO_CHANGE; + usb_status = PDA_PSY_TO_CHANGE; + break; + default: + return NOTIFY_OK; + } + + /* + * Wait a bit before reading ac/usb line status and setting charger, + * because ac/usb status readings may lag from irq. + */ + cancel_delayed_work(&charger_work); + schedule_delayed_work(&charger_work, + msecs_to_jiffies(pdata->wait_for_status)); + + return NOTIFY_OK; +} +#endif + +static int pda_power_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + int ret = 0; + + dev = &pdev->dev; + + if (pdev->id != -1) { + dev_err(dev, "it's meaningless to register several " + "pda_powers; use id = -1\n"); + ret = -EINVAL; + goto wrongid; + } + + pdata = pdev->dev.platform_data; + + if (pdata->init) { + ret = pdata->init(dev); + if (ret < 0) + goto init_failed; + } + + ac_draw = regulator_get(dev, "ac_draw"); + if (IS_ERR(ac_draw)) { + dev_dbg(dev, "couldn't get ac_draw regulator\n"); + ac_draw = NULL; + } + + update_status(); + update_charger(); + + if (!pdata->wait_for_status) + pdata->wait_for_status = 500; + + if (!pdata->wait_for_charger) + pdata->wait_for_charger = 500; + + if (!pdata->polling_interval) + pdata->polling_interval = 2000; + + if (!pdata->ac_max_uA) + pdata->ac_max_uA = 500000; + + INIT_DELAYED_WORK(&charger_work, charger_work_func); + INIT_DELAYED_WORK(&supply_work, supply_work_func); + + ac_irq = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "ac"); + usb_irq = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "usb"); + + if (pdata->supplied_to) { + psy_cfg.supplied_to = pdata->supplied_to; + psy_cfg.num_supplicants = pdata->num_supplicants; + } else { + psy_cfg.supplied_to = pda_power_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(pda_power_supplied_to); + } + +#if IS_ENABLED(CONFIG_USB_PHY) + transceiver = usb_get_phy(USB_PHY_TYPE_USB2); + if (!IS_ERR_OR_NULL(transceiver)) { + if (!pdata->is_usb_online) + pdata->is_usb_online = otg_is_usb_online; + if (!pdata->is_ac_online) + pdata->is_ac_online = otg_is_ac_online; + } +#endif + + if (pdata->is_ac_online) { + pda_psy_ac = power_supply_register(&pdev->dev, + &pda_psy_ac_desc, &psy_cfg); + if (IS_ERR(pda_psy_ac)) { + dev_err(dev, "failed to register %s power supply\n", + pda_psy_ac_desc.name); + ret = PTR_ERR(pda_psy_ac); + goto ac_supply_failed; + } + + if (ac_irq) { + ret = request_irq(ac_irq->start, power_changed_isr, + get_irq_flags(ac_irq), ac_irq->name, + pda_psy_ac); + if (ret) { + dev_err(dev, "request ac irq failed\n"); + goto ac_irq_failed; + } + } else { + polling = 1; + } + } + + if (pdata->is_usb_online) { + pda_psy_usb = power_supply_register(&pdev->dev, + &pda_psy_usb_desc, + &psy_cfg); + if (IS_ERR(pda_psy_usb)) { + dev_err(dev, "failed to register %s power supply\n", + pda_psy_usb_desc.name); + ret = PTR_ERR(pda_psy_usb); + goto usb_supply_failed; + } + + if (usb_irq) { + ret = request_irq(usb_irq->start, power_changed_isr, + get_irq_flags(usb_irq), + usb_irq->name, pda_psy_usb); + if (ret) { + dev_err(dev, "request usb irq failed\n"); + goto usb_irq_failed; + } + } else { + polling = 1; + } + } + +#if IS_ENABLED(CONFIG_USB_PHY) + if (!IS_ERR_OR_NULL(transceiver) && pdata->use_otg_notifier) { + otg_nb.notifier_call = otg_handle_notification; + ret = usb_register_notifier(transceiver, &otg_nb); + if (ret) { + dev_err(dev, "failure to register otg notifier\n"); + goto otg_reg_notifier_failed; + } + polling = 0; + } +#endif + + if (polling) { + dev_dbg(dev, "will poll for status\n"); + INIT_DELAYED_WORK(&polling_work, polling_work_func); + cancel_delayed_work(&polling_work); + schedule_delayed_work(&polling_work, + msecs_to_jiffies(pdata->polling_interval)); + } + + if (ac_irq || usb_irq) + device_init_wakeup(&pdev->dev, 1); + + return 0; + +#if IS_ENABLED(CONFIG_USB_PHY) +otg_reg_notifier_failed: + if (pdata->is_usb_online && usb_irq) + free_irq(usb_irq->start, pda_psy_usb); +#endif +usb_irq_failed: + if (pdata->is_usb_online) + power_supply_unregister(pda_psy_usb); +usb_supply_failed: + if (pdata->is_ac_online && ac_irq) + free_irq(ac_irq->start, pda_psy_ac); +#if IS_ENABLED(CONFIG_USB_PHY) + if (!IS_ERR_OR_NULL(transceiver)) + usb_put_phy(transceiver); +#endif +ac_irq_failed: + if (pdata->is_ac_online) + power_supply_unregister(pda_psy_ac); +ac_supply_failed: + if (ac_draw) { + regulator_put(ac_draw); + ac_draw = NULL; + } + if (pdata->exit) + pdata->exit(dev); +init_failed: +wrongid: + return ret; +} + +static int pda_power_remove(struct platform_device *pdev) +{ +#if IS_ENABLED(CONFIG_USB_PHY) + if (!IS_ERR_OR_NULL(transceiver) && pdata->use_otg_notifier) + usb_unregister_notifier(transceiver, &otg_nb); +#endif + if (pdata->is_usb_online && usb_irq) + free_irq(usb_irq->start, pda_psy_usb); + if (pdata->is_ac_online && ac_irq) + free_irq(ac_irq->start, pda_psy_ac); + + if (polling) + cancel_delayed_work_sync(&polling_work); + cancel_delayed_work_sync(&charger_work); + cancel_delayed_work_sync(&supply_work); + + if (pdata->is_usb_online) + power_supply_unregister(pda_psy_usb); + if (pdata->is_ac_online) + power_supply_unregister(pda_psy_ac); +#if IS_ENABLED(CONFIG_USB_PHY) + if (!IS_ERR_OR_NULL(transceiver)) + usb_put_phy(transceiver); +#endif + if (ac_draw) { + regulator_put(ac_draw); + ac_draw = NULL; + } + if (pdata->exit) + pdata->exit(dev); + + return 0; +} + +#ifdef CONFIG_PM +static int ac_wakeup_enabled; +static int usb_wakeup_enabled; + +static int pda_power_suspend(struct platform_device *pdev, pm_message_t state) +{ + if (pdata->suspend) { + int ret = pdata->suspend(state); + + if (ret) + return ret; + } + + if (device_may_wakeup(&pdev->dev)) { + if (ac_irq) + ac_wakeup_enabled = !enable_irq_wake(ac_irq->start); + if (usb_irq) + usb_wakeup_enabled = !enable_irq_wake(usb_irq->start); + } + + return 0; +} + +static int pda_power_resume(struct platform_device *pdev) +{ + if (device_may_wakeup(&pdev->dev)) { + if (usb_irq && usb_wakeup_enabled) + disable_irq_wake(usb_irq->start); + if (ac_irq && ac_wakeup_enabled) + disable_irq_wake(ac_irq->start); + } + + if (pdata->resume) + return pdata->resume(); + + return 0; +} +#else +#define pda_power_suspend NULL +#define pda_power_resume NULL +#endif /* CONFIG_PM */ + +static struct platform_driver pda_power_pdrv = { + .driver = { + .name = "pda-power", + }, + .probe = pda_power_probe, + .remove = pda_power_remove, + .suspend = pda_power_suspend, + .resume = pda_power_resume, +}; + +module_platform_driver(pda_power_pdrv); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Anton Vorontsov <cbou@mail.ru>"); +MODULE_ALIAS("platform:pda-power"); diff --git a/drivers/power/supply/pm2301_charger.c b/drivers/power/supply/pm2301_charger.c new file mode 100644 index 000000000..34f168f62 --- /dev/null +++ b/drivers/power/supply/pm2301_charger.c @@ -0,0 +1,1250 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2012 ST Ericsson. + * + * Power supply driver for ST Ericsson pm2xxx_charger charger + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regulator/consumer.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/workqueue.h> +#include <linux/mfd/abx500/ab8500.h> +#include <linux/mfd/abx500/ab8500-bm.h> +#include <linux/mfd/abx500/ux500_chargalg.h> +#include <linux/pm2301_charger.h> +#include <linux/gpio.h> +#include <linux/pm_runtime.h> +#include <linux/pm.h> + +#include "pm2301_charger.h" + +#define to_pm2xxx_charger_ac_device_info(x) container_of((x), \ + struct pm2xxx_charger, ac_chg) +#define SLEEP_MIN 50 +#define SLEEP_MAX 100 +#define PM2XXX_AUTOSUSPEND_DELAY 500 + +static int pm2xxx_interrupt_registers[] = { + PM2XXX_REG_INT1, + PM2XXX_REG_INT2, + PM2XXX_REG_INT3, + PM2XXX_REG_INT4, + PM2XXX_REG_INT5, + PM2XXX_REG_INT6, +}; + +static enum power_supply_property pm2xxx_charger_ac_props[] = { + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_AVG, +}; + +static int pm2xxx_charger_voltage_map[] = { + 3500, + 3525, + 3550, + 3575, + 3600, + 3625, + 3650, + 3675, + 3700, + 3725, + 3750, + 3775, + 3800, + 3825, + 3850, + 3875, + 3900, + 3925, + 3950, + 3975, + 4000, + 4025, + 4050, + 4075, + 4100, + 4125, + 4150, + 4175, + 4200, + 4225, + 4250, + 4275, + 4300, +}; + +static int pm2xxx_charger_current_map[] = { + 200, + 200, + 400, + 600, + 800, + 1000, + 1200, + 1400, + 1600, + 1800, + 2000, + 2200, + 2400, + 2600, + 2800, + 3000, +}; + +static void set_lpn_pin(struct pm2xxx_charger *pm2) +{ + if (!pm2->ac.charger_connected && gpio_is_valid(pm2->lpn_pin)) { + gpio_set_value(pm2->lpn_pin, 1); + usleep_range(SLEEP_MIN, SLEEP_MAX); + } +} + +static void clear_lpn_pin(struct pm2xxx_charger *pm2) +{ + if (!pm2->ac.charger_connected && gpio_is_valid(pm2->lpn_pin)) + gpio_set_value(pm2->lpn_pin, 0); +} + +static int pm2xxx_reg_read(struct pm2xxx_charger *pm2, int reg, u8 *val) +{ + int ret; + + /* wake up the device */ + pm_runtime_get_sync(pm2->dev); + + ret = i2c_smbus_read_i2c_block_data(pm2->config.pm2xxx_i2c, reg, + 1, val); + if (ret < 0) + dev_err(pm2->dev, "Error reading register at 0x%x\n", reg); + else + ret = 0; + + pm_runtime_put_sync(pm2->dev); + + return ret; +} + +static int pm2xxx_reg_write(struct pm2xxx_charger *pm2, int reg, u8 val) +{ + int ret; + + /* wake up the device */ + pm_runtime_get_sync(pm2->dev); + + ret = i2c_smbus_write_i2c_block_data(pm2->config.pm2xxx_i2c, reg, + 1, &val); + if (ret < 0) + dev_err(pm2->dev, "Error writing register at 0x%x\n", reg); + else + ret = 0; + + pm_runtime_put_sync(pm2->dev); + + return ret; +} + +static int pm2xxx_charging_enable_mngt(struct pm2xxx_charger *pm2) +{ + int ret; + + /* Enable charging */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG2, + (PM2XXX_CH_AUTO_RESUME_EN | PM2XXX_CHARGER_ENA)); + + return ret; +} + +static int pm2xxx_charging_disable_mngt(struct pm2xxx_charger *pm2) +{ + int ret; + + /* Disable SW EOC ctrl */ + ret = pm2xxx_reg_write(pm2, PM2XXX_SW_CTRL_REG, PM2XXX_SWCTRL_HW); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + return ret; + } + + /* Disable charging */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG2, + (PM2XXX_CH_AUTO_RESUME_DIS | PM2XXX_CHARGER_DIS)); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + return ret; + } + + return 0; +} + +static int pm2xxx_charger_batt_therm_mngt(struct pm2xxx_charger *pm2, int val) +{ + queue_work(pm2->charger_wq, &pm2->check_main_thermal_prot_work); + + return 0; +} + + +static int pm2xxx_charger_die_therm_mngt(struct pm2xxx_charger *pm2, int val) +{ + queue_work(pm2->charger_wq, &pm2->check_main_thermal_prot_work); + + return 0; +} + +static int pm2xxx_charger_ovv_mngt(struct pm2xxx_charger *pm2, int val) +{ + dev_err(pm2->dev, "Overvoltage detected\n"); + pm2->flags.ovv = true; + power_supply_changed(pm2->ac_chg.psy); + + /* Schedule a new HW failure check */ + queue_delayed_work(pm2->charger_wq, &pm2->check_hw_failure_work, 0); + + return 0; +} + +static int pm2xxx_charger_wd_exp_mngt(struct pm2xxx_charger *pm2, int val) +{ + dev_dbg(pm2->dev , "20 minutes watchdog expired\n"); + + pm2->ac.wd_expired = true; + power_supply_changed(pm2->ac_chg.psy); + + return 0; +} + +static int pm2xxx_charger_vbat_lsig_mngt(struct pm2xxx_charger *pm2, int val) +{ + int ret; + + switch (val) { + case PM2XXX_INT1_ITVBATLOWR: + dev_dbg(pm2->dev, "VBAT grows above VBAT_LOW level\n"); + /* Enable SW EOC ctrl */ + ret = pm2xxx_reg_write(pm2, PM2XXX_SW_CTRL_REG, + PM2XXX_SWCTRL_SW); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + return ret; + } + break; + + case PM2XXX_INT1_ITVBATLOWF: + dev_dbg(pm2->dev, "VBAT drops below VBAT_LOW level\n"); + /* Disable SW EOC ctrl */ + ret = pm2xxx_reg_write(pm2, PM2XXX_SW_CTRL_REG, + PM2XXX_SWCTRL_HW); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + return ret; + } + break; + + default: + dev_err(pm2->dev, "Unknown VBAT level\n"); + } + + return 0; +} + +static int pm2xxx_charger_bat_disc_mngt(struct pm2xxx_charger *pm2, int val) +{ + dev_dbg(pm2->dev, "battery disconnected\n"); + + return 0; +} + +static int pm2xxx_charger_detection(struct pm2xxx_charger *pm2, u8 *val) +{ + int ret; + + ret = pm2xxx_reg_read(pm2, PM2XXX_SRCE_REG_INT2, val); + + if (ret < 0) { + dev_err(pm2->dev, "Charger detection failed\n"); + goto out; + } + + *val &= (PM2XXX_INT2_S_ITVPWR1PLUG | PM2XXX_INT2_S_ITVPWR2PLUG); + +out: + return ret; +} + +static int pm2xxx_charger_itv_pwr_plug_mngt(struct pm2xxx_charger *pm2, int val) +{ + + int ret; + u8 read_val; + + /* + * Since we can't be sure that the events are received + * synchronously, we have the check if the main charger is + * connected by reading the interrupt source register. + */ + ret = pm2xxx_charger_detection(pm2, &read_val); + + if ((ret == 0) && read_val) { + pm2->ac.charger_connected = 1; + pm2->ac_conn = true; + queue_work(pm2->charger_wq, &pm2->ac_work); + } + + + return ret; +} + +static int pm2xxx_charger_itv_pwr_unplug_mngt(struct pm2xxx_charger *pm2, + int val) +{ + pm2->ac.charger_connected = 0; + queue_work(pm2->charger_wq, &pm2->ac_work); + + return 0; +} + +static int pm2_int_reg0(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & PM2XXX_INT1_ITVBATLOWR) { + ret = pm2xxx_charger_vbat_lsig_mngt(pm2, + PM2XXX_INT1_ITVBATLOWR); + if (ret < 0) + goto out; + } + + if (val & PM2XXX_INT1_ITVBATLOWF) { + ret = pm2xxx_charger_vbat_lsig_mngt(pm2, + PM2XXX_INT1_ITVBATLOWF); + if (ret < 0) + goto out; + } + + if (val & PM2XXX_INT1_ITVBATDISCONNECT) { + ret = pm2xxx_charger_bat_disc_mngt(pm2, + PM2XXX_INT1_ITVBATDISCONNECT); + if (ret < 0) + goto out; + } +out: + return ret; +} + +static int pm2_int_reg1(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & (PM2XXX_INT2_ITVPWR1PLUG | PM2XXX_INT2_ITVPWR2PLUG)) { + dev_dbg(pm2->dev , "Main charger plugged\n"); + ret = pm2xxx_charger_itv_pwr_plug_mngt(pm2, val & + (PM2XXX_INT2_ITVPWR1PLUG | PM2XXX_INT2_ITVPWR2PLUG)); + } + + if (val & + (PM2XXX_INT2_ITVPWR1UNPLUG | PM2XXX_INT2_ITVPWR2UNPLUG)) { + dev_dbg(pm2->dev , "Main charger unplugged\n"); + ret = pm2xxx_charger_itv_pwr_unplug_mngt(pm2, val & + (PM2XXX_INT2_ITVPWR1UNPLUG | + PM2XXX_INT2_ITVPWR2UNPLUG)); + } + + return ret; +} + +static int pm2_int_reg2(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & PM2XXX_INT3_ITAUTOTIMEOUTWD) + ret = pm2xxx_charger_wd_exp_mngt(pm2, val); + + if (val & (PM2XXX_INT3_ITCHPRECHARGEWD | + PM2XXX_INT3_ITCHCCWD | PM2XXX_INT3_ITCHCVWD)) { + dev_dbg(pm2->dev, + "Watchdog occurred for precharge, CC and CV charge\n"); + } + + return ret; +} + +static int pm2_int_reg3(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & (PM2XXX_INT4_ITCHARGINGON)) { + dev_dbg(pm2->dev , + "charging operation has started\n"); + } + + if (val & (PM2XXX_INT4_ITVRESUME)) { + dev_dbg(pm2->dev, + "battery discharged down to VResume threshold\n"); + } + + if (val & (PM2XXX_INT4_ITBATTFULL)) { + dev_dbg(pm2->dev , "battery fully detected\n"); + } + + if (val & (PM2XXX_INT4_ITCVPHASE)) { + dev_dbg(pm2->dev, "CV phase enter with 0.5C charging\n"); + } + + if (val & (PM2XXX_INT4_ITVPWR2OVV | PM2XXX_INT4_ITVPWR1OVV)) { + pm2->failure_case = VPWR_OVV; + ret = pm2xxx_charger_ovv_mngt(pm2, val & + (PM2XXX_INT4_ITVPWR2OVV | PM2XXX_INT4_ITVPWR1OVV)); + dev_dbg(pm2->dev, "VPWR/VSYSTEM overvoltage detected\n"); + } + + if (val & (PM2XXX_INT4_S_ITBATTEMPCOLD | + PM2XXX_INT4_S_ITBATTEMPHOT)) { + ret = pm2xxx_charger_batt_therm_mngt(pm2, val & + (PM2XXX_INT4_S_ITBATTEMPCOLD | + PM2XXX_INT4_S_ITBATTEMPHOT)); + dev_dbg(pm2->dev, "BTEMP is too Low/High\n"); + } + + return ret; +} + +static int pm2_int_reg4(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & PM2XXX_INT5_ITVSYSTEMOVV) { + pm2->failure_case = VSYSTEM_OVV; + ret = pm2xxx_charger_ovv_mngt(pm2, val & + PM2XXX_INT5_ITVSYSTEMOVV); + dev_dbg(pm2->dev, "VSYSTEM overvoltage detected\n"); + } + + if (val & (PM2XXX_INT5_ITTHERMALWARNINGFALL | + PM2XXX_INT5_ITTHERMALWARNINGRISE | + PM2XXX_INT5_ITTHERMALSHUTDOWNFALL | + PM2XXX_INT5_ITTHERMALSHUTDOWNRISE)) { + dev_dbg(pm2->dev, "BTEMP die temperature is too Low/High\n"); + ret = pm2xxx_charger_die_therm_mngt(pm2, val & + (PM2XXX_INT5_ITTHERMALWARNINGFALL | + PM2XXX_INT5_ITTHERMALWARNINGRISE | + PM2XXX_INT5_ITTHERMALSHUTDOWNFALL | + PM2XXX_INT5_ITTHERMALSHUTDOWNRISE)); + } + + return ret; +} + +static int pm2_int_reg5(void *pm2_data, int val) +{ + struct pm2xxx_charger *pm2 = pm2_data; + int ret = 0; + + if (val & (PM2XXX_INT6_ITVPWR2DROP | PM2XXX_INT6_ITVPWR1DROP)) { + dev_dbg(pm2->dev, "VMPWR drop to VBAT level\n"); + } + + if (val & (PM2XXX_INT6_ITVPWR2VALIDRISE | + PM2XXX_INT6_ITVPWR1VALIDRISE | + PM2XXX_INT6_ITVPWR2VALIDFALL | + PM2XXX_INT6_ITVPWR1VALIDFALL)) { + dev_dbg(pm2->dev, "Falling/Rising edge on WPWR1/2\n"); + } + + return ret; +} + +static irqreturn_t pm2xxx_irq_int(int irq, void *data) +{ + struct pm2xxx_charger *pm2 = data; + struct pm2xxx_interrupts *interrupt = pm2->pm2_int; + int i; + + /* wake up the device */ + pm_runtime_get_sync(pm2->dev); + + do { + for (i = 0; i < PM2XXX_NUM_INT_REG; i++) { + pm2xxx_reg_read(pm2, + pm2xxx_interrupt_registers[i], + &(interrupt->reg[i])); + + if (interrupt->reg[i] > 0) + interrupt->handler[i](pm2, interrupt->reg[i]); + } + } while (gpio_get_value(pm2->pdata->gpio_irq_number) == 0); + + pm_runtime_mark_last_busy(pm2->dev); + pm_runtime_put_autosuspend(pm2->dev); + + return IRQ_HANDLED; +} + +static int pm2xxx_charger_get_ac_cv(struct pm2xxx_charger *pm2) +{ + int ret = 0; + u8 val; + + if (pm2->ac.charger_connected && pm2->ac.charger_online) { + + ret = pm2xxx_reg_read(pm2, PM2XXX_SRCE_REG_INT4, &val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx read failed\n", __func__); + goto out; + } + + if (val & PM2XXX_INT4_S_ITCVPHASE) + ret = PM2XXX_CONST_VOLT; + else + ret = PM2XXX_CONST_CURR; + } +out: + return ret; +} + +static int pm2xxx_current_to_regval(int curr) +{ + int i; + + if (curr < pm2xxx_charger_current_map[0]) + return 0; + + for (i = 1; i < ARRAY_SIZE(pm2xxx_charger_current_map); i++) { + if (curr < pm2xxx_charger_current_map[i]) + return (i - 1); + } + + i = ARRAY_SIZE(pm2xxx_charger_current_map) - 1; + if (curr == pm2xxx_charger_current_map[i]) + return i; + else + return -EINVAL; +} + +static int pm2xxx_voltage_to_regval(int curr) +{ + int i; + + if (curr < pm2xxx_charger_voltage_map[0]) + return 0; + + for (i = 1; i < ARRAY_SIZE(pm2xxx_charger_voltage_map); i++) { + if (curr < pm2xxx_charger_voltage_map[i]) + return i - 1; + } + + i = ARRAY_SIZE(pm2xxx_charger_voltage_map) - 1; + if (curr == pm2xxx_charger_voltage_map[i]) + return i; + else + return -EINVAL; +} + +static int pm2xxx_charger_update_charger_current(struct ux500_charger *charger, + int ich_out) +{ + int ret; + int curr_index; + struct pm2xxx_charger *pm2; + u8 val; + + if (charger->psy->desc->type == POWER_SUPPLY_TYPE_MAINS) + pm2 = to_pm2xxx_charger_ac_device_info(charger); + else + return -ENXIO; + + curr_index = pm2xxx_current_to_regval(ich_out); + if (curr_index < 0) { + dev_err(pm2->dev, + "Charger current too high, charging not started\n"); + return -ENXIO; + } + + ret = pm2xxx_reg_read(pm2, PM2XXX_BATT_CTRL_REG6, &val); + if (ret >= 0) { + val &= ~PM2XXX_DIR_CH_CC_CURRENT_MASK; + val |= curr_index; + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG6, val); + if (ret < 0) { + dev_err(pm2->dev, + "%s write failed\n", __func__); + } + } + else + dev_err(pm2->dev, "%s read failed\n", __func__); + + return ret; +} + +static int pm2xxx_charger_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pm2xxx_charger *pm2; + + pm2 = to_pm2xxx_charger_ac_device_info(psy_to_ux500_charger(psy)); + + switch (psp) { + case POWER_SUPPLY_PROP_HEALTH: + if (pm2->flags.mainextchnotok) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else if (pm2->ac.wd_expired) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (pm2->flags.main_thermal_prot) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (pm2->flags.ovv) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = pm2->ac.charger_online; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = pm2->ac.charger_connected; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + pm2->ac.cv_active = pm2xxx_charger_get_ac_cv(pm2); + val->intval = pm2->ac.cv_active; + break; + default: + return -EINVAL; + } + return 0; +} + +static int pm2xxx_charging_init(struct pm2xxx_charger *pm2) +{ + int ret = 0; + + /* enable CC and CV watchdog */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG3, + (PM2XXX_CH_WD_CV_PHASE_60MIN | PM2XXX_CH_WD_CC_PHASE_60MIN)); + if( ret < 0) + return ret; + + /* enable precharge watchdog */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG4, + PM2XXX_CH_WD_PRECH_PHASE_60MIN); + + /* Disable auto timeout */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG5, + PM2XXX_CH_WD_AUTO_TIMEOUT_20MIN); + + /* + * EOC current level = 100mA + * Precharge current level = 100mA + * CC current level = 1000mA + */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG6, + (PM2XXX_DIR_CH_CC_CURRENT_1000MA | + PM2XXX_CH_PRECH_CURRENT_100MA | + PM2XXX_CH_EOC_CURRENT_100MA)); + + /* + * recharge threshold = 3.8V + * Precharge to CC threshold = 2.9V + */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG7, + (PM2XXX_CH_PRECH_VOL_2_9 | PM2XXX_CH_VRESUME_VOL_3_8)); + + /* float voltage charger level = 4.2V */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG8, + PM2XXX_CH_VOLT_4_2); + + /* Voltage drop between VBAT and VSYS in HW charging = 300mV */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG9, + (PM2XXX_CH_150MV_DROP_300MV | PM2XXX_CHARCHING_INFO_DIS | + PM2XXX_CH_CC_REDUCED_CURRENT_IDENT | + PM2XXX_CH_CC_MODEDROP_DIS)); + + /* Input charger level of over voltage = 10V */ + ret = pm2xxx_reg_write(pm2, PM2XXX_INP_VOLT_VPWR2, + PM2XXX_VPWR2_OVV_10); + ret = pm2xxx_reg_write(pm2, PM2XXX_INP_VOLT_VPWR1, + PM2XXX_VPWR1_OVV_10); + + /* Input charger drop */ + ret = pm2xxx_reg_write(pm2, PM2XXX_INP_DROP_VPWR2, + (PM2XXX_VPWR2_HW_OPT_DIS | PM2XXX_VPWR2_VALID_DIS | + PM2XXX_VPWR2_DROP_DIS)); + ret = pm2xxx_reg_write(pm2, PM2XXX_INP_DROP_VPWR1, + (PM2XXX_VPWR1_HW_OPT_DIS | PM2XXX_VPWR1_VALID_DIS | + PM2XXX_VPWR1_DROP_DIS)); + + /* Disable battery low monitoring */ + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_LOW_LEV_COMP_REG, + PM2XXX_VBAT_LOW_MONITORING_ENA); + + return ret; +} + +static int pm2xxx_charger_ac_en(struct ux500_charger *charger, + int enable, int vset, int iset) +{ + int ret; + int volt_index; + int curr_index; + u8 val; + + struct pm2xxx_charger *pm2 = to_pm2xxx_charger_ac_device_info(charger); + + if (enable) { + if (!pm2->ac.charger_connected) { + dev_dbg(pm2->dev, "AC charger not connected\n"); + return -ENXIO; + } + + dev_dbg(pm2->dev, "Enable AC: %dmV %dmA\n", vset, iset); + if (!pm2->vddadc_en_ac) { + ret = regulator_enable(pm2->regu); + if (ret) + dev_warn(pm2->dev, + "Failed to enable vddadc regulator\n"); + else + pm2->vddadc_en_ac = true; + } + + ret = pm2xxx_charging_init(pm2); + if (ret < 0) { + dev_err(pm2->dev, "%s charging init failed\n", + __func__); + goto error_occured; + } + + volt_index = pm2xxx_voltage_to_regval(vset); + curr_index = pm2xxx_current_to_regval(iset); + + if (volt_index < 0 || curr_index < 0) { + dev_err(pm2->dev, + "Charger voltage or current too high, " + "charging not started\n"); + return -ENXIO; + } + + ret = pm2xxx_reg_read(pm2, PM2XXX_BATT_CTRL_REG8, &val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx read failed\n", __func__); + goto error_occured; + } + val &= ~PM2XXX_CH_VOLT_MASK; + val |= volt_index; + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG8, val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + goto error_occured; + } + + ret = pm2xxx_reg_read(pm2, PM2XXX_BATT_CTRL_REG6, &val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx read failed\n", __func__); + goto error_occured; + } + val &= ~PM2XXX_DIR_CH_CC_CURRENT_MASK; + val |= curr_index; + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_CTRL_REG6, val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", __func__); + goto error_occured; + } + + if (!pm2->bat->enable_overshoot) { + ret = pm2xxx_reg_read(pm2, PM2XXX_LED_CTRL_REG, &val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx read failed\n", + __func__); + goto error_occured; + } + val |= PM2XXX_ANTI_OVERSHOOT_EN; + ret = pm2xxx_reg_write(pm2, PM2XXX_LED_CTRL_REG, val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx write failed\n", + __func__); + goto error_occured; + } + } + + ret = pm2xxx_charging_enable_mngt(pm2); + if (ret < 0) { + dev_err(pm2->dev, "Failed to enable" + "pm2xxx ac charger\n"); + goto error_occured; + } + + pm2->ac.charger_online = 1; + } else { + pm2->ac.charger_online = 0; + pm2->ac.wd_expired = false; + + /* Disable regulator if enabled */ + if (pm2->vddadc_en_ac) { + regulator_disable(pm2->regu); + pm2->vddadc_en_ac = false; + } + + ret = pm2xxx_charging_disable_mngt(pm2); + if (ret < 0) { + dev_err(pm2->dev, "failed to disable" + "pm2xxx ac charger\n"); + goto error_occured; + } + + dev_dbg(pm2->dev, "PM2301: " "Disabled AC charging\n"); + } + power_supply_changed(pm2->ac_chg.psy); + +error_occured: + return ret; +} + +static int pm2xxx_charger_watchdog_kick(struct ux500_charger *charger) +{ + int ret; + struct pm2xxx_charger *pm2; + + if (charger->psy->desc->type == POWER_SUPPLY_TYPE_MAINS) + pm2 = to_pm2xxx_charger_ac_device_info(charger); + else + return -ENXIO; + + ret = pm2xxx_reg_write(pm2, PM2XXX_BATT_WD_KICK, WD_TIMER); + if (ret) + dev_err(pm2->dev, "Failed to kick WD!\n"); + + return ret; +} + +static void pm2xxx_charger_ac_work(struct work_struct *work) +{ + struct pm2xxx_charger *pm2 = container_of(work, + struct pm2xxx_charger, ac_work); + + + power_supply_changed(pm2->ac_chg.psy); + sysfs_notify(&pm2->ac_chg.psy->dev.kobj, NULL, "present"); +}; + +static void pm2xxx_charger_check_hw_failure_work(struct work_struct *work) +{ + u8 reg_value; + + struct pm2xxx_charger *pm2 = container_of(work, + struct pm2xxx_charger, check_hw_failure_work.work); + + if (pm2->flags.ovv) { + pm2xxx_reg_read(pm2, PM2XXX_SRCE_REG_INT4, ®_value); + + if (!(reg_value & (PM2XXX_INT4_S_ITVPWR1OVV | + PM2XXX_INT4_S_ITVPWR2OVV))) { + pm2->flags.ovv = false; + power_supply_changed(pm2->ac_chg.psy); + } + } + + /* If we still have a failure, schedule a new check */ + if (pm2->flags.ovv) { + queue_delayed_work(pm2->charger_wq, + &pm2->check_hw_failure_work, round_jiffies(HZ)); + } +} + +static void pm2xxx_charger_check_main_thermal_prot_work( + struct work_struct *work) +{ + int ret; + u8 val; + + struct pm2xxx_charger *pm2 = container_of(work, struct pm2xxx_charger, + check_main_thermal_prot_work); + + /* Check if die temp warning is still active */ + ret = pm2xxx_reg_read(pm2, PM2XXX_SRCE_REG_INT5, &val); + if (ret < 0) { + dev_err(pm2->dev, "%s pm2xxx read failed\n", __func__); + return; + } + if (val & (PM2XXX_INT5_S_ITTHERMALWARNINGRISE + | PM2XXX_INT5_S_ITTHERMALSHUTDOWNRISE)) + pm2->flags.main_thermal_prot = true; + else if (val & (PM2XXX_INT5_S_ITTHERMALWARNINGFALL + | PM2XXX_INT5_S_ITTHERMALSHUTDOWNFALL)) + pm2->flags.main_thermal_prot = false; + + power_supply_changed(pm2->ac_chg.psy); +} + +static struct pm2xxx_interrupts pm2xxx_int = { + .handler[0] = pm2_int_reg0, + .handler[1] = pm2_int_reg1, + .handler[2] = pm2_int_reg2, + .handler[3] = pm2_int_reg3, + .handler[4] = pm2_int_reg4, + .handler[5] = pm2_int_reg5, +}; + +static struct pm2xxx_irq pm2xxx_charger_irq[] = { + {"PM2XXX_IRQ_INT", pm2xxx_irq_int}, +}; + +static int __maybe_unused pm2xxx_wall_charger_resume(struct device *dev) +{ + struct i2c_client *i2c_client = to_i2c_client(dev); + struct pm2xxx_charger *pm2; + + pm2 = (struct pm2xxx_charger *)i2c_get_clientdata(i2c_client); + set_lpn_pin(pm2); + + /* If we still have a HW failure, schedule a new check */ + if (pm2->flags.ovv) + queue_delayed_work(pm2->charger_wq, + &pm2->check_hw_failure_work, 0); + + return 0; +} + +static int __maybe_unused pm2xxx_wall_charger_suspend(struct device *dev) +{ + struct i2c_client *i2c_client = to_i2c_client(dev); + struct pm2xxx_charger *pm2; + + pm2 = (struct pm2xxx_charger *)i2c_get_clientdata(i2c_client); + clear_lpn_pin(pm2); + + /* Cancel any pending HW failure check */ + if (delayed_work_pending(&pm2->check_hw_failure_work)) + cancel_delayed_work(&pm2->check_hw_failure_work); + + flush_work(&pm2->ac_work); + flush_work(&pm2->check_main_thermal_prot_work); + + return 0; +} + +static int __maybe_unused pm2xxx_runtime_suspend(struct device *dev) +{ + struct i2c_client *pm2xxx_i2c_client = to_i2c_client(dev); + struct pm2xxx_charger *pm2; + + pm2 = (struct pm2xxx_charger *)i2c_get_clientdata(pm2xxx_i2c_client); + clear_lpn_pin(pm2); + + return 0; +} + +static int __maybe_unused pm2xxx_runtime_resume(struct device *dev) +{ + struct i2c_client *pm2xxx_i2c_client = to_i2c_client(dev); + struct pm2xxx_charger *pm2; + + pm2 = (struct pm2xxx_charger *)i2c_get_clientdata(pm2xxx_i2c_client); + + if (gpio_is_valid(pm2->lpn_pin) && gpio_get_value(pm2->lpn_pin) == 0) + set_lpn_pin(pm2); + + return 0; +} + +static const struct dev_pm_ops pm2xxx_pm_ops __maybe_unused = { + SET_SYSTEM_SLEEP_PM_OPS(pm2xxx_wall_charger_suspend, + pm2xxx_wall_charger_resume) + SET_RUNTIME_PM_OPS(pm2xxx_runtime_suspend, pm2xxx_runtime_resume, NULL) +}; + +static int pm2xxx_wall_charger_probe(struct i2c_client *i2c_client, + const struct i2c_device_id *id) +{ + struct pm2xxx_platform_data *pl_data = i2c_client->dev.platform_data; + struct power_supply_config psy_cfg = {}; + struct pm2xxx_charger *pm2; + int ret = 0; + u8 val; + int i; + + if (!pl_data) { + dev_err(&i2c_client->dev, "No platform data supplied\n"); + return -EINVAL; + } + + pm2 = kzalloc(sizeof(struct pm2xxx_charger), GFP_KERNEL); + if (!pm2) { + dev_err(&i2c_client->dev, "pm2xxx_charger allocation failed\n"); + return -ENOMEM; + } + + /* get parent data */ + pm2->dev = &i2c_client->dev; + + pm2->pm2_int = &pm2xxx_int; + + /* get charger spcific platform data */ + if (!pl_data->wall_charger) { + dev_err(pm2->dev, "no charger platform data supplied\n"); + ret = -EINVAL; + goto free_device_info; + } + + pm2->pdata = pl_data->wall_charger; + + /* get battery specific platform data */ + if (!pl_data->battery) { + dev_err(pm2->dev, "no battery platform data supplied\n"); + ret = -EINVAL; + goto free_device_info; + } + + pm2->bat = pl_data->battery; + + if (!i2c_check_functionality(i2c_client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_READ_WORD_DATA)) { + ret = -ENODEV; + dev_info(pm2->dev, "pm2301 i2c_check_functionality failed\n"); + goto free_device_info; + } + + pm2->config.pm2xxx_i2c = i2c_client; + pm2->config.pm2xxx_id = (struct i2c_device_id *) id; + i2c_set_clientdata(i2c_client, pm2); + + /* AC supply */ + /* power_supply base class */ + pm2->ac_chg_desc.name = pm2->pdata->label; + pm2->ac_chg_desc.type = POWER_SUPPLY_TYPE_MAINS; + pm2->ac_chg_desc.properties = pm2xxx_charger_ac_props; + pm2->ac_chg_desc.num_properties = ARRAY_SIZE(pm2xxx_charger_ac_props); + pm2->ac_chg_desc.get_property = pm2xxx_charger_ac_get_property; + + psy_cfg.supplied_to = pm2->pdata->supplied_to; + psy_cfg.num_supplicants = pm2->pdata->num_supplicants; + /* pm2xxx_charger sub-class */ + pm2->ac_chg.ops.enable = &pm2xxx_charger_ac_en; + pm2->ac_chg.ops.kick_wd = &pm2xxx_charger_watchdog_kick; + pm2->ac_chg.ops.update_curr = &pm2xxx_charger_update_charger_current; + pm2->ac_chg.max_out_volt = pm2xxx_charger_voltage_map[ + ARRAY_SIZE(pm2xxx_charger_voltage_map) - 1]; + pm2->ac_chg.max_out_curr = pm2xxx_charger_current_map[ + ARRAY_SIZE(pm2xxx_charger_current_map) - 1]; + pm2->ac_chg.wdt_refresh = WD_KICK_INTERVAL; + pm2->ac_chg.enabled = true; + pm2->ac_chg.external = true; + + /* Create a work queue for the charger */ + pm2->charger_wq = alloc_ordered_workqueue("pm2xxx_charger_wq", + WQ_MEM_RECLAIM); + if (pm2->charger_wq == NULL) { + ret = -ENOMEM; + dev_err(pm2->dev, "failed to create work queue\n"); + goto free_device_info; + } + + /* Init work for charger detection */ + INIT_WORK(&pm2->ac_work, pm2xxx_charger_ac_work); + + /* Init work for checking HW status */ + INIT_WORK(&pm2->check_main_thermal_prot_work, + pm2xxx_charger_check_main_thermal_prot_work); + + /* Init work for HW failure check */ + INIT_DEFERRABLE_WORK(&pm2->check_hw_failure_work, + pm2xxx_charger_check_hw_failure_work); + + /* + * VDD ADC supply needs to be enabled from this driver when there + * is a charger connected to avoid erroneous BTEMP_HIGH/LOW + * interrupts during charging + */ + pm2->regu = regulator_get(pm2->dev, "vddadc"); + if (IS_ERR(pm2->regu)) { + ret = PTR_ERR(pm2->regu); + dev_err(pm2->dev, "failed to get vddadc regulator\n"); + goto free_charger_wq; + } + + /* Register AC charger class */ + pm2->ac_chg.psy = power_supply_register(pm2->dev, &pm2->ac_chg_desc, + &psy_cfg); + if (IS_ERR(pm2->ac_chg.psy)) { + dev_err(pm2->dev, "failed to register AC charger\n"); + ret = PTR_ERR(pm2->ac_chg.psy); + goto free_regulator; + } + + /* Register interrupts */ + ret = request_threaded_irq(gpio_to_irq(pm2->pdata->gpio_irq_number), + NULL, + pm2xxx_charger_irq[0].isr, + pm2->pdata->irq_type | IRQF_ONESHOT, + pm2xxx_charger_irq[0].name, pm2); + + if (ret != 0) { + dev_err(pm2->dev, "failed to request %s IRQ %d: %d\n", + pm2xxx_charger_irq[0].name, + gpio_to_irq(pm2->pdata->gpio_irq_number), ret); + goto unregister_pm2xxx_charger; + } + + ret = pm_runtime_set_active(pm2->dev); + if (ret) + dev_err(pm2->dev, "set active Error\n"); + + pm_runtime_enable(pm2->dev); + pm_runtime_set_autosuspend_delay(pm2->dev, PM2XXX_AUTOSUSPEND_DELAY); + pm_runtime_use_autosuspend(pm2->dev); + pm_runtime_resume(pm2->dev); + + /* pm interrupt can wake up system */ + ret = enable_irq_wake(gpio_to_irq(pm2->pdata->gpio_irq_number)); + if (ret) { + dev_err(pm2->dev, "failed to set irq wake\n"); + goto unregister_pm2xxx_interrupt; + } + + mutex_init(&pm2->lock); + + if (gpio_is_valid(pm2->pdata->lpn_gpio)) { + /* get lpn GPIO from platform data */ + pm2->lpn_pin = pm2->pdata->lpn_gpio; + + /* + * Charger detection mechanism requires pulling up the LPN pin + * while i2c communication if Charger is not connected + * LPN pin of PM2301 is GPIO60 of AB9540 + */ + ret = gpio_request(pm2->lpn_pin, "pm2301_lpm_gpio"); + + if (ret < 0) { + dev_err(pm2->dev, "pm2301_lpm_gpio request failed\n"); + goto disable_pm2_irq_wake; + } + ret = gpio_direction_output(pm2->lpn_pin, 0); + if (ret < 0) { + dev_err(pm2->dev, "pm2301_lpm_gpio direction failed\n"); + goto free_gpio; + } + set_lpn_pin(pm2); + } + + /* read interrupt registers */ + for (i = 0; i < PM2XXX_NUM_INT_REG; i++) + pm2xxx_reg_read(pm2, + pm2xxx_interrupt_registers[i], + &val); + + ret = pm2xxx_charger_detection(pm2, &val); + + if ((ret == 0) && val) { + pm2->ac.charger_connected = 1; + ab8500_override_turn_on_stat(~AB8500_POW_KEY_1_ON, + AB8500_MAIN_CH_DET); + pm2->ac_conn = true; + power_supply_changed(pm2->ac_chg.psy); + sysfs_notify(&pm2->ac_chg.psy->dev.kobj, NULL, "present"); + } + + return 0; + +free_gpio: + if (gpio_is_valid(pm2->lpn_pin)) + gpio_free(pm2->lpn_pin); +disable_pm2_irq_wake: + disable_irq_wake(gpio_to_irq(pm2->pdata->gpio_irq_number)); +unregister_pm2xxx_interrupt: + /* disable interrupt */ + free_irq(gpio_to_irq(pm2->pdata->gpio_irq_number), pm2); +unregister_pm2xxx_charger: + /* unregister power supply */ + power_supply_unregister(pm2->ac_chg.psy); +free_regulator: + /* disable the regulator */ + regulator_put(pm2->regu); +free_charger_wq: + destroy_workqueue(pm2->charger_wq); +free_device_info: + kfree(pm2); + + return ret; +} + +static int pm2xxx_wall_charger_remove(struct i2c_client *i2c_client) +{ + struct pm2xxx_charger *pm2 = i2c_get_clientdata(i2c_client); + + /* Disable pm_runtime */ + pm_runtime_disable(pm2->dev); + /* Disable AC charging */ + pm2xxx_charger_ac_en(&pm2->ac_chg, false, 0, 0); + + /* Disable wake by pm interrupt */ + disable_irq_wake(gpio_to_irq(pm2->pdata->gpio_irq_number)); + + /* Disable interrupts */ + free_irq(gpio_to_irq(pm2->pdata->gpio_irq_number), pm2); + + /* Delete the work queue */ + destroy_workqueue(pm2->charger_wq); + + flush_scheduled_work(); + + /* disable the regulator */ + regulator_put(pm2->regu); + + power_supply_unregister(pm2->ac_chg.psy); + + if (gpio_is_valid(pm2->lpn_pin)) + gpio_free(pm2->lpn_pin); + + kfree(pm2); + + return 0; +} + +static const struct i2c_device_id pm2xxx_id[] = { + { "pm2301", 0 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pm2xxx_id); + +static struct i2c_driver pm2xxx_charger_driver = { + .probe = pm2xxx_wall_charger_probe, + .remove = pm2xxx_wall_charger_remove, + .driver = { + .name = "pm2xxx-wall_charger", + .pm = IS_ENABLED(CONFIG_PM) ? &pm2xxx_pm_ops : NULL, + }, + .id_table = pm2xxx_id, +}; + +static int __init pm2xxx_charger_init(void) +{ + return i2c_add_driver(&pm2xxx_charger_driver); +} + +static void __exit pm2xxx_charger_exit(void) +{ + i2c_del_driver(&pm2xxx_charger_driver); +} + +device_initcall_sync(pm2xxx_charger_init); +module_exit(pm2xxx_charger_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Rajkumar kasirajan, Olivier Launay"); +MODULE_DESCRIPTION("PM2xxx charger management driver"); diff --git a/drivers/power/supply/pm2301_charger.h b/drivers/power/supply/pm2301_charger.h new file mode 100644 index 000000000..74397e377 --- /dev/null +++ b/drivers/power/supply/pm2301_charger.h @@ -0,0 +1,492 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) ST-Ericsson SA 2012 + * + * PM2301 power supply interface + */ + +#ifndef PM2301_CHARGER_H +#define PM2301_CHARGER_H + +/* Watchdog timeout constant */ +#define WD_TIMER 0x30 /* 4min */ +#define WD_KICK_INTERVAL (30 * HZ) + +#define PM2XXX_NUM_INT_REG 0x6 + +/* Constant voltage/current */ +#define PM2XXX_CONST_CURR 0x0 +#define PM2XXX_CONST_VOLT 0x1 + +/* Lowest charger voltage is 3.39V -> 0x4E */ +#define LOW_VOLT_REG 0x4E + +#define PM2XXX_BATT_CTRL_REG1 0x00 +#define PM2XXX_BATT_CTRL_REG2 0x01 +#define PM2XXX_BATT_CTRL_REG3 0x02 +#define PM2XXX_BATT_CTRL_REG4 0x03 +#define PM2XXX_BATT_CTRL_REG5 0x04 +#define PM2XXX_BATT_CTRL_REG6 0x05 +#define PM2XXX_BATT_CTRL_REG7 0x06 +#define PM2XXX_BATT_CTRL_REG8 0x07 +#define PM2XXX_NTC_CTRL_REG1 0x08 +#define PM2XXX_NTC_CTRL_REG2 0x09 +#define PM2XXX_BATT_CTRL_REG9 0x0A +#define PM2XXX_BATT_STAT_REG1 0x0B +#define PM2XXX_INP_VOLT_VPWR2 0x11 +#define PM2XXX_INP_DROP_VPWR2 0x13 +#define PM2XXX_INP_VOLT_VPWR1 0x15 +#define PM2XXX_INP_DROP_VPWR1 0x17 +#define PM2XXX_INP_MODE_VPWR 0x18 +#define PM2XXX_BATT_WD_KICK 0x70 +#define PM2XXX_DEV_VER_STAT 0x0C +#define PM2XXX_THERM_WARN_CTRL_REG 0x20 +#define PM2XXX_BATT_DISC_REG 0x21 +#define PM2XXX_BATT_LOW_LEV_COMP_REG 0x22 +#define PM2XXX_BATT_LOW_LEV_VAL_REG 0x23 +#define PM2XXX_I2C_PAD_CTRL_REG 0x24 +#define PM2XXX_SW_CTRL_REG 0x26 +#define PM2XXX_LED_CTRL_REG 0x28 + +#define PM2XXX_REG_INT1 0x40 +#define PM2XXX_MASK_REG_INT1 0x50 +#define PM2XXX_SRCE_REG_INT1 0x60 +#define PM2XXX_REG_INT2 0x41 +#define PM2XXX_MASK_REG_INT2 0x51 +#define PM2XXX_SRCE_REG_INT2 0x61 +#define PM2XXX_REG_INT3 0x42 +#define PM2XXX_MASK_REG_INT3 0x52 +#define PM2XXX_SRCE_REG_INT3 0x62 +#define PM2XXX_REG_INT4 0x43 +#define PM2XXX_MASK_REG_INT4 0x53 +#define PM2XXX_SRCE_REG_INT4 0x63 +#define PM2XXX_REG_INT5 0x44 +#define PM2XXX_MASK_REG_INT5 0x54 +#define PM2XXX_SRCE_REG_INT5 0x64 +#define PM2XXX_REG_INT6 0x45 +#define PM2XXX_MASK_REG_INT6 0x55 +#define PM2XXX_SRCE_REG_INT6 0x65 + +#define VPWR_OVV 0x0 +#define VSYSTEM_OVV 0x1 + +/* control Reg 1 */ +#define PM2XXX_CH_RESUME_EN 0x1 +#define PM2XXX_CH_RESUME_DIS 0x0 + +/* control Reg 2 */ +#define PM2XXX_CH_AUTO_RESUME_EN 0X2 +#define PM2XXX_CH_AUTO_RESUME_DIS 0X0 +#define PM2XXX_CHARGER_ENA 0x4 +#define PM2XXX_CHARGER_DIS 0x0 + +/* control Reg 3 */ +#define PM2XXX_CH_WD_CC_PHASE_OFF 0x0 +#define PM2XXX_CH_WD_CC_PHASE_5MIN 0x1 +#define PM2XXX_CH_WD_CC_PHASE_10MIN 0x2 +#define PM2XXX_CH_WD_CC_PHASE_30MIN 0x3 +#define PM2XXX_CH_WD_CC_PHASE_60MIN 0x4 +#define PM2XXX_CH_WD_CC_PHASE_120MIN 0x5 +#define PM2XXX_CH_WD_CC_PHASE_240MIN 0x6 +#define PM2XXX_CH_WD_CC_PHASE_360MIN 0x7 + +#define PM2XXX_CH_WD_CV_PHASE_OFF (0x0<<3) +#define PM2XXX_CH_WD_CV_PHASE_5MIN (0x1<<3) +#define PM2XXX_CH_WD_CV_PHASE_10MIN (0x2<<3) +#define PM2XXX_CH_WD_CV_PHASE_30MIN (0x3<<3) +#define PM2XXX_CH_WD_CV_PHASE_60MIN (0x4<<3) +#define PM2XXX_CH_WD_CV_PHASE_120MIN (0x5<<3) +#define PM2XXX_CH_WD_CV_PHASE_240MIN (0x6<<3) +#define PM2XXX_CH_WD_CV_PHASE_360MIN (0x7<<3) + +/* control Reg 4 */ +#define PM2XXX_CH_WD_PRECH_PHASE_OFF 0x0 +#define PM2XXX_CH_WD_PRECH_PHASE_1MIN 0x1 +#define PM2XXX_CH_WD_PRECH_PHASE_5MIN 0x2 +#define PM2XXX_CH_WD_PRECH_PHASE_10MIN 0x3 +#define PM2XXX_CH_WD_PRECH_PHASE_30MIN 0x4 +#define PM2XXX_CH_WD_PRECH_PHASE_60MIN 0x5 +#define PM2XXX_CH_WD_PRECH_PHASE_120MIN 0x6 +#define PM2XXX_CH_WD_PRECH_PHASE_240MIN 0x7 + +/* control Reg 5 */ +#define PM2XXX_CH_WD_AUTO_TIMEOUT_NONE 0x0 +#define PM2XXX_CH_WD_AUTO_TIMEOUT_20MIN 0x1 + +/* control Reg 6 */ +#define PM2XXX_DIR_CH_CC_CURRENT_MASK 0x0F +#define PM2XXX_DIR_CH_CC_CURRENT_200MA 0x0 +#define PM2XXX_DIR_CH_CC_CURRENT_400MA 0x2 +#define PM2XXX_DIR_CH_CC_CURRENT_600MA 0x3 +#define PM2XXX_DIR_CH_CC_CURRENT_800MA 0x4 +#define PM2XXX_DIR_CH_CC_CURRENT_1000MA 0x5 +#define PM2XXX_DIR_CH_CC_CURRENT_1200MA 0x6 +#define PM2XXX_DIR_CH_CC_CURRENT_1400MA 0x7 +#define PM2XXX_DIR_CH_CC_CURRENT_1600MA 0x8 +#define PM2XXX_DIR_CH_CC_CURRENT_1800MA 0x9 +#define PM2XXX_DIR_CH_CC_CURRENT_2000MA 0xA +#define PM2XXX_DIR_CH_CC_CURRENT_2200MA 0xB +#define PM2XXX_DIR_CH_CC_CURRENT_2400MA 0xC +#define PM2XXX_DIR_CH_CC_CURRENT_2600MA 0xD +#define PM2XXX_DIR_CH_CC_CURRENT_2800MA 0xE +#define PM2XXX_DIR_CH_CC_CURRENT_3000MA 0xF + +#define PM2XXX_CH_PRECH_CURRENT_MASK 0x30 +#define PM2XXX_CH_PRECH_CURRENT_25MA (0x0<<4) +#define PM2XXX_CH_PRECH_CURRENT_50MA (0x1<<4) +#define PM2XXX_CH_PRECH_CURRENT_75MA (0x2<<4) +#define PM2XXX_CH_PRECH_CURRENT_100MA (0x3<<4) + +#define PM2XXX_CH_EOC_CURRENT_MASK 0xC0 +#define PM2XXX_CH_EOC_CURRENT_100MA (0x0<<6) +#define PM2XXX_CH_EOC_CURRENT_150MA (0x1<<6) +#define PM2XXX_CH_EOC_CURRENT_300MA (0x2<<6) +#define PM2XXX_CH_EOC_CURRENT_400MA (0x3<<6) + +/* control Reg 7 */ +#define PM2XXX_CH_PRECH_VOL_2_5 0x0 +#define PM2XXX_CH_PRECH_VOL_2_7 0x1 +#define PM2XXX_CH_PRECH_VOL_2_9 0x2 +#define PM2XXX_CH_PRECH_VOL_3_1 0x3 + +#define PM2XXX_CH_VRESUME_VOL_3_2 (0x0<<2) +#define PM2XXX_CH_VRESUME_VOL_3_4 (0x1<<2) +#define PM2XXX_CH_VRESUME_VOL_3_6 (0x2<<2) +#define PM2XXX_CH_VRESUME_VOL_3_8 (0x3<<2) + +/* control Reg 8 */ +#define PM2XXX_CH_VOLT_MASK 0x3F +#define PM2XXX_CH_VOLT_3_5 0x0 +#define PM2XXX_CH_VOLT_3_5225 0x1 +#define PM2XXX_CH_VOLT_3_6 0x4 +#define PM2XXX_CH_VOLT_3_7 0x8 +#define PM2XXX_CH_VOLT_4_0 0x14 +#define PM2XXX_CH_VOLT_4_175 0x1B +#define PM2XXX_CH_VOLT_4_2 0x1C +#define PM2XXX_CH_VOLT_4_275 0x1F +#define PM2XXX_CH_VOLT_4_3 0x20 + +/*NTC control register 1*/ +#define PM2XXX_BTEMP_HIGH_TH_45 0x0 +#define PM2XXX_BTEMP_HIGH_TH_50 0x1 +#define PM2XXX_BTEMP_HIGH_TH_55 0x2 +#define PM2XXX_BTEMP_HIGH_TH_60 0x3 +#define PM2XXX_BTEMP_HIGH_TH_65 0x4 + +#define PM2XXX_BTEMP_LOW_TH_N5 (0x0<<3) +#define PM2XXX_BTEMP_LOW_TH_0 (0x1<<3) +#define PM2XXX_BTEMP_LOW_TH_5 (0x2<<3) +#define PM2XXX_BTEMP_LOW_TH_10 (0x3<<3) + +/*NTC control register 2*/ +#define PM2XXX_NTC_BETA_COEFF_3477 0x0 +#define PM2XXX_NTC_BETA_COEFF_3964 0x1 + +#define PM2XXX_NTC_RES_10K (0x0<<2) +#define PM2XXX_NTC_RES_47K (0x1<<2) +#define PM2XXX_NTC_RES_100K (0x2<<2) +#define PM2XXX_NTC_RES_NO_NTC (0x3<<2) + +/* control Reg 9 */ +#define PM2XXX_CH_CC_MODEDROP_EN 1 +#define PM2XXX_CH_CC_MODEDROP_DIS 0 + +#define PM2XXX_CH_CC_REDUCED_CURRENT_100MA (0x0<<1) +#define PM2XXX_CH_CC_REDUCED_CURRENT_200MA (0x1<<1) +#define PM2XXX_CH_CC_REDUCED_CURRENT_400MA (0x2<<1) +#define PM2XXX_CH_CC_REDUCED_CURRENT_IDENT (0x3<<1) + +#define PM2XXX_CHARCHING_INFO_DIS (0<<3) +#define PM2XXX_CHARCHING_INFO_EN (1<<3) + +#define PM2XXX_CH_150MV_DROP_300MV (0<<4) +#define PM2XXX_CH_150MV_DROP_150MV (1<<4) + + +/* charger status register */ +#define PM2XXX_CHG_STATUS_OFF 0x0 +#define PM2XXX_CHG_STATUS_ON 0x1 +#define PM2XXX_CHG_STATUS_FULL 0x2 +#define PM2XXX_CHG_STATUS_ERR 0x3 +#define PM2XXX_CHG_STATUS_WAIT 0x4 +#define PM2XXX_CHG_STATUS_NOBAT 0x5 + +/* Input charger voltage VPWR2 */ +#define PM2XXX_VPWR2_OVV_6_0 0x0 +#define PM2XXX_VPWR2_OVV_6_3 0x1 +#define PM2XXX_VPWR2_OVV_10 0x2 +#define PM2XXX_VPWR2_OVV_NONE 0x3 + +/* Input charger drop VPWR2 */ +#define PM2XXX_VPWR2_HW_OPT_EN (0x1<<4) +#define PM2XXX_VPWR2_HW_OPT_DIS (0x0<<4) + +#define PM2XXX_VPWR2_VALID_EN (0x1<<3) +#define PM2XXX_VPWR2_VALID_DIS (0x0<<3) + +#define PM2XXX_VPWR2_DROP_EN (0x1<<2) +#define PM2XXX_VPWR2_DROP_DIS (0x0<<2) + +/* Input charger voltage VPWR1 */ +#define PM2XXX_VPWR1_OVV_6_0 0x0 +#define PM2XXX_VPWR1_OVV_6_3 0x1 +#define PM2XXX_VPWR1_OVV_10 0x2 +#define PM2XXX_VPWR1_OVV_NONE 0x3 + +/* Input charger drop VPWR1 */ +#define PM2XXX_VPWR1_HW_OPT_EN (0x1<<4) +#define PM2XXX_VPWR1_HW_OPT_DIS (0x0<<4) + +#define PM2XXX_VPWR1_VALID_EN (0x1<<3) +#define PM2XXX_VPWR1_VALID_DIS (0x0<<3) + +#define PM2XXX_VPWR1_DROP_EN (0x1<<2) +#define PM2XXX_VPWR1_DROP_DIS (0x0<<2) + +/* Battery low level comparator control register */ +#define PM2XXX_VBAT_LOW_MONITORING_DIS 0x0 +#define PM2XXX_VBAT_LOW_MONITORING_ENA 0x1 + +/* Battery low level value control register */ +#define PM2XXX_VBAT_LOW_LEVEL_2_3 0x0 +#define PM2XXX_VBAT_LOW_LEVEL_2_4 0x1 +#define PM2XXX_VBAT_LOW_LEVEL_2_5 0x2 +#define PM2XXX_VBAT_LOW_LEVEL_2_6 0x3 +#define PM2XXX_VBAT_LOW_LEVEL_2_7 0x4 +#define PM2XXX_VBAT_LOW_LEVEL_2_8 0x5 +#define PM2XXX_VBAT_LOW_LEVEL_2_9 0x6 +#define PM2XXX_VBAT_LOW_LEVEL_3_0 0x7 +#define PM2XXX_VBAT_LOW_LEVEL_3_1 0x8 +#define PM2XXX_VBAT_LOW_LEVEL_3_2 0x9 +#define PM2XXX_VBAT_LOW_LEVEL_3_3 0xA +#define PM2XXX_VBAT_LOW_LEVEL_3_4 0xB +#define PM2XXX_VBAT_LOW_LEVEL_3_5 0xC +#define PM2XXX_VBAT_LOW_LEVEL_3_6 0xD +#define PM2XXX_VBAT_LOW_LEVEL_3_7 0xE +#define PM2XXX_VBAT_LOW_LEVEL_3_8 0xF +#define PM2XXX_VBAT_LOW_LEVEL_3_9 0x10 +#define PM2XXX_VBAT_LOW_LEVEL_4_0 0x11 +#define PM2XXX_VBAT_LOW_LEVEL_4_1 0x12 +#define PM2XXX_VBAT_LOW_LEVEL_4_2 0x13 + +/* SW CTRL */ +#define PM2XXX_SWCTRL_HW 0x0 +#define PM2XXX_SWCTRL_SW 0x1 + + +/* LED Driver Control */ +#define PM2XXX_LED_CURRENT_MASK 0x0C +#define PM2XXX_LED_CURRENT_2_5MA (0X0<<2) +#define PM2XXX_LED_CURRENT_1MA (0X1<<2) +#define PM2XXX_LED_CURRENT_5MA (0X2<<2) +#define PM2XXX_LED_CURRENT_10MA (0X3<<2) + +#define PM2XXX_LED_SELECT_MASK 0x02 +#define PM2XXX_LED_SELECT_EN (0X0<<1) +#define PM2XXX_LED_SELECT_DIS (0X1<<1) + +#define PM2XXX_ANTI_OVERSHOOT_MASK 0x01 +#define PM2XXX_ANTI_OVERSHOOT_DIS 0X0 +#define PM2XXX_ANTI_OVERSHOOT_EN 0X1 + +enum pm2xxx_reg_int1 { + PM2XXX_INT1_ITVBATDISCONNECT = 0x02, + PM2XXX_INT1_ITVBATLOWR = 0x04, + PM2XXX_INT1_ITVBATLOWF = 0x08, +}; + +enum pm2xxx_mask_reg_int1 { + PM2XXX_INT1_M_ITVBATDISCONNECT = 0x02, + PM2XXX_INT1_M_ITVBATLOWR = 0x04, + PM2XXX_INT1_M_ITVBATLOWF = 0x08, +}; + +enum pm2xxx_source_reg_int1 { + PM2XXX_INT1_S_ITVBATDISCONNECT = 0x02, + PM2XXX_INT1_S_ITVBATLOWR = 0x04, + PM2XXX_INT1_S_ITVBATLOWF = 0x08, +}; + +enum pm2xxx_reg_int2 { + PM2XXX_INT2_ITVPWR2PLUG = 0x01, + PM2XXX_INT2_ITVPWR2UNPLUG = 0x02, + PM2XXX_INT2_ITVPWR1PLUG = 0x04, + PM2XXX_INT2_ITVPWR1UNPLUG = 0x08, +}; + +enum pm2xxx_mask_reg_int2 { + PM2XXX_INT2_M_ITVPWR2PLUG = 0x01, + PM2XXX_INT2_M_ITVPWR2UNPLUG = 0x02, + PM2XXX_INT2_M_ITVPWR1PLUG = 0x04, + PM2XXX_INT2_M_ITVPWR1UNPLUG = 0x08, +}; + +enum pm2xxx_source_reg_int2 { + PM2XXX_INT2_S_ITVPWR2PLUG = 0x03, + PM2XXX_INT2_S_ITVPWR1PLUG = 0x0c, +}; + +enum pm2xxx_reg_int3 { + PM2XXX_INT3_ITCHPRECHARGEWD = 0x01, + PM2XXX_INT3_ITCHCCWD = 0x02, + PM2XXX_INT3_ITCHCVWD = 0x04, + PM2XXX_INT3_ITAUTOTIMEOUTWD = 0x08, +}; + +enum pm2xxx_mask_reg_int3 { + PM2XXX_INT3_M_ITCHPRECHARGEWD = 0x01, + PM2XXX_INT3_M_ITCHCCWD = 0x02, + PM2XXX_INT3_M_ITCHCVWD = 0x04, + PM2XXX_INT3_M_ITAUTOTIMEOUTWD = 0x08, +}; + +enum pm2xxx_source_reg_int3 { + PM2XXX_INT3_S_ITCHPRECHARGEWD = 0x01, + PM2XXX_INT3_S_ITCHCCWD = 0x02, + PM2XXX_INT3_S_ITCHCVWD = 0x04, + PM2XXX_INT3_S_ITAUTOTIMEOUTWD = 0x08, +}; + +enum pm2xxx_reg_int4 { + PM2XXX_INT4_ITBATTEMPCOLD = 0x01, + PM2XXX_INT4_ITBATTEMPHOT = 0x02, + PM2XXX_INT4_ITVPWR2OVV = 0x04, + PM2XXX_INT4_ITVPWR1OVV = 0x08, + PM2XXX_INT4_ITCHARGINGON = 0x10, + PM2XXX_INT4_ITVRESUME = 0x20, + PM2XXX_INT4_ITBATTFULL = 0x40, + PM2XXX_INT4_ITCVPHASE = 0x80, +}; + +enum pm2xxx_mask_reg_int4 { + PM2XXX_INT4_M_ITBATTEMPCOLD = 0x01, + PM2XXX_INT4_M_ITBATTEMPHOT = 0x02, + PM2XXX_INT4_M_ITVPWR2OVV = 0x04, + PM2XXX_INT4_M_ITVPWR1OVV = 0x08, + PM2XXX_INT4_M_ITCHARGINGON = 0x10, + PM2XXX_INT4_M_ITVRESUME = 0x20, + PM2XXX_INT4_M_ITBATTFULL = 0x40, + PM2XXX_INT4_M_ITCVPHASE = 0x80, +}; + +enum pm2xxx_source_reg_int4 { + PM2XXX_INT4_S_ITBATTEMPCOLD = 0x01, + PM2XXX_INT4_S_ITBATTEMPHOT = 0x02, + PM2XXX_INT4_S_ITVPWR2OVV = 0x04, + PM2XXX_INT4_S_ITVPWR1OVV = 0x08, + PM2XXX_INT4_S_ITCHARGINGON = 0x10, + PM2XXX_INT4_S_ITVRESUME = 0x20, + PM2XXX_INT4_S_ITBATTFULL = 0x40, + PM2XXX_INT4_S_ITCVPHASE = 0x80, +}; + +enum pm2xxx_reg_int5 { + PM2XXX_INT5_ITTHERMALSHUTDOWNRISE = 0x01, + PM2XXX_INT5_ITTHERMALSHUTDOWNFALL = 0x02, + PM2XXX_INT5_ITTHERMALWARNINGRISE = 0x04, + PM2XXX_INT5_ITTHERMALWARNINGFALL = 0x08, + PM2XXX_INT5_ITVSYSTEMOVV = 0x10, +}; + +enum pm2xxx_mask_reg_int5 { + PM2XXX_INT5_M_ITTHERMALSHUTDOWNRISE = 0x01, + PM2XXX_INT5_M_ITTHERMALSHUTDOWNFALL = 0x02, + PM2XXX_INT5_M_ITTHERMALWARNINGRISE = 0x04, + PM2XXX_INT5_M_ITTHERMALWARNINGFALL = 0x08, + PM2XXX_INT5_M_ITVSYSTEMOVV = 0x10, +}; + +enum pm2xxx_source_reg_int5 { + PM2XXX_INT5_S_ITTHERMALSHUTDOWNRISE = 0x01, + PM2XXX_INT5_S_ITTHERMALSHUTDOWNFALL = 0x02, + PM2XXX_INT5_S_ITTHERMALWARNINGRISE = 0x04, + PM2XXX_INT5_S_ITTHERMALWARNINGFALL = 0x08, + PM2XXX_INT5_S_ITVSYSTEMOVV = 0x10, +}; + +enum pm2xxx_reg_int6 { + PM2XXX_INT6_ITVPWR2DROP = 0x01, + PM2XXX_INT6_ITVPWR1DROP = 0x02, + PM2XXX_INT6_ITVPWR2VALIDRISE = 0x04, + PM2XXX_INT6_ITVPWR2VALIDFALL = 0x08, + PM2XXX_INT6_ITVPWR1VALIDRISE = 0x10, + PM2XXX_INT6_ITVPWR1VALIDFALL = 0x20, +}; + +enum pm2xxx_mask_reg_int6 { + PM2XXX_INT6_M_ITVPWR2DROP = 0x01, + PM2XXX_INT6_M_ITVPWR1DROP = 0x02, + PM2XXX_INT6_M_ITVPWR2VALIDRISE = 0x04, + PM2XXX_INT6_M_ITVPWR2VALIDFALL = 0x08, + PM2XXX_INT6_M_ITVPWR1VALIDRISE = 0x10, + PM2XXX_INT6_M_ITVPWR1VALIDFALL = 0x20, +}; + +enum pm2xxx_source_reg_int6 { + PM2XXX_INT6_S_ITVPWR2DROP = 0x01, + PM2XXX_INT6_S_ITVPWR1DROP = 0x02, + PM2XXX_INT6_S_ITVPWR2VALIDRISE = 0x04, + PM2XXX_INT6_S_ITVPWR2VALIDFALL = 0x08, + PM2XXX_INT6_S_ITVPWR1VALIDRISE = 0x10, + PM2XXX_INT6_S_ITVPWR1VALIDFALL = 0x20, +}; + +struct pm2xxx_charger_info { + int charger_connected; + int charger_online; + int cv_active; + bool wd_expired; +}; + +struct pm2xxx_charger_event_flags { + bool mainextchnotok; + bool main_thermal_prot; + bool ovv; + bool chgwdexp; +}; + +struct pm2xxx_interrupts { + u8 reg[PM2XXX_NUM_INT_REG]; + int (*handler[PM2XXX_NUM_INT_REG])(void *, int); +}; + +struct pm2xxx_config { + struct i2c_client *pm2xxx_i2c; + struct i2c_device_id *pm2xxx_id; +}; + +struct pm2xxx_irq { + char *name; + irqreturn_t (*isr)(int irq, void *data); +}; + +struct pm2xxx_charger { + struct device *dev; + u8 chip_id; + bool vddadc_en_ac; + struct pm2xxx_config config; + bool ac_conn; + unsigned int gpio_irq; + int vbat; + int old_vbat; + int failure_case; + int failure_input_ovv; + unsigned int lpn_pin; + struct pm2xxx_interrupts *pm2_int; + struct regulator *regu; + struct pm2xxx_bm_data *bat; + struct mutex lock; + struct ab8500 *parent; + struct pm2xxx_charger_info ac; + struct pm2xxx_charger_platform_data *pdata; + struct workqueue_struct *charger_wq; + struct delayed_work check_vbat_work; + struct work_struct ac_work; + struct work_struct check_main_thermal_prot_work; + struct delayed_work check_hw_failure_work; + struct ux500_charger ac_chg; + struct power_supply_desc ac_chg_desc; + struct pm2xxx_charger_event_flags flags; +}; + +#endif /* PM2301_CHARGER_H */ diff --git a/drivers/power/supply/pmu_battery.c b/drivers/power/supply/pmu_battery.c new file mode 100644 index 000000000..eaab7500d --- /dev/null +++ b/drivers/power/supply/pmu_battery.c @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery class driver for Apple PMU + * + * Copyright © 2006 David Woodhouse <dwmw2@infradead.org> + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/power_supply.h> +#include <linux/adb.h> +#include <linux/pmu.h> +#include <linux/slab.h> + +static struct pmu_battery_dev { + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct pmu_battery_info *pbi; + char name[16]; + int propval; +} *pbats[PMU_MAX_BATTERIES]; + +#define to_pmu_battery_dev(x) power_supply_get_drvdata(x) + +/********************************************************************* + * Power + *********************************************************************/ + +static int pmu_get_ac_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = (!!(pmu_power_flags & PMU_PWR_AC_PRESENT)) || + (pmu_battery_count == 0); + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property pmu_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc pmu_ac_desc = { + .name = "pmu-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = pmu_ac_props, + .num_properties = ARRAY_SIZE(pmu_ac_props), + .get_property = pmu_get_ac_prop, +}; + +static struct power_supply *pmu_ac; + +/********************************************************************* + * Battery properties + *********************************************************************/ + +static char *pmu_batt_types[] = { + "Smart", "Comet", "Hooper", "Unknown" +}; + +static char *pmu_bat_get_model_name(struct pmu_battery_info *pbi) +{ + switch (pbi->flags & PMU_BATT_TYPE_MASK) { + case PMU_BATT_TYPE_SMART: + return pmu_batt_types[0]; + case PMU_BATT_TYPE_COMET: + return pmu_batt_types[1]; + case PMU_BATT_TYPE_HOOPER: + return pmu_batt_types[2]; + default: break; + } + return pmu_batt_types[3]; +} + +static int pmu_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pmu_battery_dev *pbat = to_pmu_battery_dev(psy); + struct pmu_battery_info *pbi = pbat->pbi; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (pbi->flags & PMU_BATT_CHARGING) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (pmu_power_flags & PMU_PWR_AC_PRESENT) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(pbi->flags & PMU_BATT_PRESENT); + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = pmu_bat_get_model_name(pbi); + break; + case POWER_SUPPLY_PROP_ENERGY_AVG: + val->intval = pbi->charge * 1000; /* mWh -> µWh */ + break; + case POWER_SUPPLY_PROP_ENERGY_FULL: + val->intval = pbi->max_charge * 1000; /* mWh -> µWh */ + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + val->intval = pbi->amperage * 1000; /* mA -> µA */ + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + val->intval = pbi->voltage * 1000; /* mV -> µV */ + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + val->intval = pbi->time_remaining; + break; + default: + return -EINVAL; + } + + return 0; +} + +static enum power_supply_property pmu_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_ENERGY_AVG, + POWER_SUPPLY_PROP_ENERGY_FULL, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, +}; + +/********************************************************************* + * Initialisation + *********************************************************************/ + +static struct platform_device *bat_pdev; + +static int __init pmu_bat_init(void) +{ + int ret = 0; + int i; + + bat_pdev = platform_device_register_simple("pmu-battery", + 0, NULL, 0); + if (IS_ERR(bat_pdev)) { + ret = PTR_ERR(bat_pdev); + goto pdev_register_failed; + } + + pmu_ac = power_supply_register(&bat_pdev->dev, &pmu_ac_desc, NULL); + if (IS_ERR(pmu_ac)) { + ret = PTR_ERR(pmu_ac); + goto ac_register_failed; + } + + for (i = 0; i < pmu_battery_count; i++) { + struct power_supply_config psy_cfg = {}; + struct pmu_battery_dev *pbat = kzalloc(sizeof(*pbat), + GFP_KERNEL); + if (!pbat) + break; + + sprintf(pbat->name, "PMU_battery_%d", i); + pbat->bat_desc.name = pbat->name; + pbat->bat_desc.properties = pmu_bat_props; + pbat->bat_desc.num_properties = ARRAY_SIZE(pmu_bat_props); + pbat->bat_desc.get_property = pmu_bat_get_property; + pbat->pbi = &pmu_batteries[i]; + psy_cfg.drv_data = pbat; + + pbat->bat = power_supply_register(&bat_pdev->dev, + &pbat->bat_desc, + &psy_cfg); + if (IS_ERR(pbat->bat)) { + ret = PTR_ERR(pbat->bat); + kfree(pbat); + goto battery_register_failed; + } + pbats[i] = pbat; + } + + goto success; + +battery_register_failed: + while (i--) { + if (!pbats[i]) + continue; + power_supply_unregister(pbats[i]->bat); + kfree(pbats[i]); + } + power_supply_unregister(pmu_ac); +ac_register_failed: + platform_device_unregister(bat_pdev); +pdev_register_failed: +success: + return ret; +} + +static void __exit pmu_bat_exit(void) +{ + int i; + + for (i = 0; i < PMU_MAX_BATTERIES; i++) { + if (!pbats[i]) + continue; + power_supply_unregister(pbats[i]->bat); + kfree(pbats[i]); + } + power_supply_unregister(pmu_ac); + platform_device_unregister(bat_pdev); +} + +module_init(pmu_bat_init); +module_exit(pmu_bat_exit); + +MODULE_AUTHOR("David Woodhouse <dwmw2@infradead.org>"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("PMU battery driver"); diff --git a/drivers/power/supply/power_supply.h b/drivers/power/supply/power_supply.h new file mode 100644 index 000000000..c310d4f36 --- /dev/null +++ b/drivers/power/supply/power_supply.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Functions private to power supply class + * + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + * Copyright © 2004 Szabolcs Gyurko + * Copyright © 2003 Ian Molton <spyro@f2s.com> + * + * Modified: 2004, Oct Szabolcs Gyurko + */ + +struct device; +struct device_type; +struct power_supply; + +#ifdef CONFIG_SYSFS + +extern void power_supply_init_attrs(struct device_type *dev_type); +extern int power_supply_uevent(struct device *dev, struct kobj_uevent_env *env); + +#else + +static inline void power_supply_init_attrs(struct device_type *dev_type) {} +#define power_supply_uevent NULL + +#endif /* CONFIG_SYSFS */ + +#ifdef CONFIG_LEDS_TRIGGERS + +extern void power_supply_update_leds(struct power_supply *psy); +extern int power_supply_create_triggers(struct power_supply *psy); +extern void power_supply_remove_triggers(struct power_supply *psy); + +#else + +static inline void power_supply_update_leds(struct power_supply *psy) {} +static inline int power_supply_create_triggers(struct power_supply *psy) +{ return 0; } +static inline void power_supply_remove_triggers(struct power_supply *psy) {} + +#endif /* CONFIG_LEDS_TRIGGERS */ diff --git a/drivers/power/supply/power_supply_core.c b/drivers/power/supply/power_supply_core.c new file mode 100644 index 000000000..5c8c117b3 --- /dev/null +++ b/drivers/power/supply/power_supply_core.c @@ -0,0 +1,1403 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Universal power supply monitor class + * + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + * Copyright © 2004 Szabolcs Gyurko + * Copyright © 2003 Ian Molton <spyro@f2s.com> + * + * Modified: 2004, Oct Szabolcs Gyurko + */ + +#include <linux/module.h> +#include <linux/types.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/notifier.h> +#include <linux/err.h> +#include <linux/of.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/thermal.h> +#include "power_supply.h" + +/* exported for the APM Power driver, APM emulation */ +struct class *power_supply_class; +EXPORT_SYMBOL_GPL(power_supply_class); + +ATOMIC_NOTIFIER_HEAD(power_supply_notifier); +EXPORT_SYMBOL_GPL(power_supply_notifier); + +static struct device_type power_supply_dev_type; + +#define POWER_SUPPLY_DEFERRED_REGISTER_TIME msecs_to_jiffies(10) + +static bool __power_supply_is_supplied_by(struct power_supply *supplier, + struct power_supply *supply) +{ + int i; + + if (!supply->supplied_from && !supplier->supplied_to) + return false; + + /* Support both supplied_to and supplied_from modes */ + if (supply->supplied_from) { + if (!supplier->desc->name) + return false; + for (i = 0; i < supply->num_supplies; i++) + if (!strcmp(supplier->desc->name, supply->supplied_from[i])) + return true; + } else { + if (!supply->desc->name) + return false; + for (i = 0; i < supplier->num_supplicants; i++) + if (!strcmp(supplier->supplied_to[i], supply->desc->name)) + return true; + } + + return false; +} + +static int __power_supply_changed_work(struct device *dev, void *data) +{ + struct power_supply *psy = data; + struct power_supply *pst = dev_get_drvdata(dev); + + if (__power_supply_is_supplied_by(psy, pst)) { + if (pst->desc->external_power_changed) + pst->desc->external_power_changed(pst); + } + + return 0; +} + +static void power_supply_changed_work(struct work_struct *work) +{ + unsigned long flags; + struct power_supply *psy = container_of(work, struct power_supply, + changed_work); + + dev_dbg(&psy->dev, "%s\n", __func__); + + spin_lock_irqsave(&psy->changed_lock, flags); + /* + * Check 'changed' here to avoid issues due to race between + * power_supply_changed() and this routine. In worst case + * power_supply_changed() can be called again just before we take above + * lock. During the first call of this routine we will mark 'changed' as + * false and it will stay false for the next call as well. + */ + if (likely(psy->changed)) { + psy->changed = false; + spin_unlock_irqrestore(&psy->changed_lock, flags); + class_for_each_device(power_supply_class, NULL, psy, + __power_supply_changed_work); + power_supply_update_leds(psy); + atomic_notifier_call_chain(&power_supply_notifier, + PSY_EVENT_PROP_CHANGED, psy); + kobject_uevent(&psy->dev.kobj, KOBJ_CHANGE); + spin_lock_irqsave(&psy->changed_lock, flags); + } + + /* + * Hold the wakeup_source until all events are processed. + * power_supply_changed() might have called again and have set 'changed' + * to true. + */ + if (likely(!psy->changed)) + pm_relax(&psy->dev); + spin_unlock_irqrestore(&psy->changed_lock, flags); +} + +void power_supply_changed(struct power_supply *psy) +{ + unsigned long flags; + + dev_dbg(&psy->dev, "%s\n", __func__); + + spin_lock_irqsave(&psy->changed_lock, flags); + psy->changed = true; + pm_stay_awake(&psy->dev); + spin_unlock_irqrestore(&psy->changed_lock, flags); + schedule_work(&psy->changed_work); +} +EXPORT_SYMBOL_GPL(power_supply_changed); + +/* + * Notify that power supply was registered after parent finished the probing. + * + * Often power supply is registered from driver's probe function. However + * calling power_supply_changed() directly from power_supply_register() + * would lead to execution of get_property() function provided by the driver + * too early - before the probe ends. + * + * Avoid that by waiting on parent's mutex. + */ +static void power_supply_deferred_register_work(struct work_struct *work) +{ + struct power_supply *psy = container_of(work, struct power_supply, + deferred_register_work.work); + + if (psy->dev.parent) { + while (!mutex_trylock(&psy->dev.parent->mutex)) { + if (psy->removing) + return; + msleep(10); + } + } + + power_supply_changed(psy); + + if (psy->dev.parent) + mutex_unlock(&psy->dev.parent->mutex); +} + +#ifdef CONFIG_OF +static int __power_supply_populate_supplied_from(struct device *dev, + void *data) +{ + struct power_supply *psy = data; + struct power_supply *epsy = dev_get_drvdata(dev); + struct device_node *np; + int i = 0; + + do { + np = of_parse_phandle(psy->of_node, "power-supplies", i++); + if (!np) + break; + + if (np == epsy->of_node) { + dev_info(&psy->dev, "%s: Found supply : %s\n", + psy->desc->name, epsy->desc->name); + psy->supplied_from[i-1] = (char *)epsy->desc->name; + psy->num_supplies++; + of_node_put(np); + break; + } + of_node_put(np); + } while (np); + + return 0; +} + +static int power_supply_populate_supplied_from(struct power_supply *psy) +{ + int error; + + error = class_for_each_device(power_supply_class, NULL, psy, + __power_supply_populate_supplied_from); + + dev_dbg(&psy->dev, "%s %d\n", __func__, error); + + return error; +} + +static int __power_supply_find_supply_from_node(struct device *dev, + void *data) +{ + struct device_node *np = data; + struct power_supply *epsy = dev_get_drvdata(dev); + + /* returning non-zero breaks out of class_for_each_device loop */ + if (epsy->of_node == np) + return 1; + + return 0; +} + +static int power_supply_find_supply_from_node(struct device_node *supply_node) +{ + int error; + + /* + * class_for_each_device() either returns its own errors or values + * returned by __power_supply_find_supply_from_node(). + * + * __power_supply_find_supply_from_node() will return 0 (no match) + * or 1 (match). + * + * We return 0 if class_for_each_device() returned 1, -EPROBE_DEFER if + * it returned 0, or error as returned by it. + */ + error = class_for_each_device(power_supply_class, NULL, supply_node, + __power_supply_find_supply_from_node); + + return error ? (error == 1 ? 0 : error) : -EPROBE_DEFER; +} + +static int power_supply_check_supplies(struct power_supply *psy) +{ + struct device_node *np; + int cnt = 0; + + /* If there is already a list honor it */ + if (psy->supplied_from && psy->num_supplies > 0) + return 0; + + /* No device node found, nothing to do */ + if (!psy->of_node) + return 0; + + do { + int ret; + + np = of_parse_phandle(psy->of_node, "power-supplies", cnt++); + if (!np) + break; + + ret = power_supply_find_supply_from_node(np); + of_node_put(np); + + if (ret) { + dev_dbg(&psy->dev, "Failed to find supply!\n"); + return ret; + } + } while (np); + + /* Missing valid "power-supplies" entries */ + if (cnt == 1) + return 0; + + /* All supplies found, allocate char ** array for filling */ + psy->supplied_from = devm_kzalloc(&psy->dev, sizeof(psy->supplied_from), + GFP_KERNEL); + if (!psy->supplied_from) + return -ENOMEM; + + *psy->supplied_from = devm_kcalloc(&psy->dev, + cnt - 1, sizeof(char *), + GFP_KERNEL); + if (!*psy->supplied_from) + return -ENOMEM; + + return power_supply_populate_supplied_from(psy); +} +#else +static int power_supply_check_supplies(struct power_supply *psy) +{ + int nval, ret; + + if (!psy->dev.parent) + return 0; + + nval = device_property_read_string_array(psy->dev.parent, + "supplied-from", NULL, 0); + if (nval <= 0) + return 0; + + psy->supplied_from = devm_kmalloc_array(&psy->dev, nval, + sizeof(char *), GFP_KERNEL); + if (!psy->supplied_from) + return -ENOMEM; + + ret = device_property_read_string_array(psy->dev.parent, + "supplied-from", (const char **)psy->supplied_from, nval); + if (ret < 0) + return ret; + + psy->num_supplies = nval; + + return 0; +} +#endif + +struct psy_am_i_supplied_data { + struct power_supply *psy; + unsigned int count; +}; + +static int __power_supply_am_i_supplied(struct device *dev, void *_data) +{ + union power_supply_propval ret = {0,}; + struct power_supply *epsy = dev_get_drvdata(dev); + struct psy_am_i_supplied_data *data = _data; + + if (__power_supply_is_supplied_by(epsy, data->psy)) { + data->count++; + if (!epsy->desc->get_property(epsy, POWER_SUPPLY_PROP_ONLINE, + &ret)) + return ret.intval; + } + + return 0; +} + +int power_supply_am_i_supplied(struct power_supply *psy) +{ + struct psy_am_i_supplied_data data = { psy, 0 }; + int error; + + error = class_for_each_device(power_supply_class, NULL, &data, + __power_supply_am_i_supplied); + + dev_dbg(&psy->dev, "%s count %u err %d\n", __func__, data.count, error); + + if (data.count == 0) + return -ENODEV; + + return error; +} +EXPORT_SYMBOL_GPL(power_supply_am_i_supplied); + +static int __power_supply_is_system_supplied(struct device *dev, void *data) +{ + union power_supply_propval ret = {0,}; + struct power_supply *psy = dev_get_drvdata(dev); + unsigned int *count = data; + + if (!psy->desc->get_property(psy, POWER_SUPPLY_PROP_SCOPE, &ret)) + if (ret.intval == POWER_SUPPLY_SCOPE_DEVICE) + return 0; + + (*count)++; + if (psy->desc->type != POWER_SUPPLY_TYPE_BATTERY) + if (!psy->desc->get_property(psy, POWER_SUPPLY_PROP_ONLINE, + &ret)) + return ret.intval; + + return 0; +} + +int power_supply_is_system_supplied(void) +{ + int error; + unsigned int count = 0; + + error = class_for_each_device(power_supply_class, NULL, &count, + __power_supply_is_system_supplied); + + /* + * If no system scope power class device was found at all, most probably we + * are running on a desktop system, so assume we are on mains power. + */ + if (count == 0) + return 1; + + return error; +} +EXPORT_SYMBOL_GPL(power_supply_is_system_supplied); + +struct psy_get_supplier_prop_data { + struct power_supply *psy; + enum power_supply_property psp; + union power_supply_propval *val; +}; + +static int __power_supply_get_supplier_property(struct device *dev, void *_data) +{ + struct power_supply *epsy = dev_get_drvdata(dev); + struct psy_get_supplier_prop_data *data = _data; + + if (__power_supply_is_supplied_by(epsy, data->psy)) + if (!epsy->desc->get_property(epsy, data->psp, data->val)) + return 1; /* Success */ + + return 0; /* Continue iterating */ +} + +int power_supply_get_property_from_supplier(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct psy_get_supplier_prop_data data = { + .psy = psy, + .psp = psp, + .val = val, + }; + int ret; + + /* + * This function is not intended for use with a supply with multiple + * suppliers, we simply pick the first supply to report the psp. + */ + ret = class_for_each_device(power_supply_class, NULL, &data, + __power_supply_get_supplier_property); + if (ret < 0) + return ret; + if (ret == 0) + return -ENODEV; + + return 0; +} +EXPORT_SYMBOL_GPL(power_supply_get_property_from_supplier); + +int power_supply_set_battery_charged(struct power_supply *psy) +{ + if (atomic_read(&psy->use_cnt) >= 0 && + psy->desc->type == POWER_SUPPLY_TYPE_BATTERY && + psy->desc->set_charged) { + psy->desc->set_charged(psy); + return 0; + } + + return -EINVAL; +} +EXPORT_SYMBOL_GPL(power_supply_set_battery_charged); + +static int power_supply_match_device_by_name(struct device *dev, const void *data) +{ + const char *name = data; + struct power_supply *psy = dev_get_drvdata(dev); + + return strcmp(psy->desc->name, name) == 0; +} + +/** + * power_supply_get_by_name() - Search for a power supply and returns its ref + * @name: Power supply name to fetch + * + * If power supply was found, it increases reference count for the + * internal power supply's device. The user should power_supply_put() + * after usage. + * + * Return: On success returns a reference to a power supply with + * matching name equals to @name, a NULL otherwise. + */ +struct power_supply *power_supply_get_by_name(const char *name) +{ + struct power_supply *psy = NULL; + struct device *dev = class_find_device(power_supply_class, NULL, name, + power_supply_match_device_by_name); + + if (dev) { + psy = dev_get_drvdata(dev); + atomic_inc(&psy->use_cnt); + } + + return psy; +} +EXPORT_SYMBOL_GPL(power_supply_get_by_name); + +/** + * power_supply_put() - Drop reference obtained with power_supply_get_by_name + * @psy: Reference to put + * + * The reference to power supply should be put before unregistering + * the power supply. + */ +void power_supply_put(struct power_supply *psy) +{ + might_sleep(); + + atomic_dec(&psy->use_cnt); + put_device(&psy->dev); +} +EXPORT_SYMBOL_GPL(power_supply_put); + +#ifdef CONFIG_OF +static int power_supply_match_device_node(struct device *dev, const void *data) +{ + return dev->parent && dev->parent->of_node == data; +} + +/** + * power_supply_get_by_phandle() - Search for a power supply and returns its ref + * @np: Pointer to device node holding phandle property + * @property: Name of property holding a power supply name + * + * If power supply was found, it increases reference count for the + * internal power supply's device. The user should power_supply_put() + * after usage. + * + * Return: On success returns a reference to a power supply with + * matching name equals to value under @property, NULL or ERR_PTR otherwise. + */ +struct power_supply *power_supply_get_by_phandle(struct device_node *np, + const char *property) +{ + struct device_node *power_supply_np; + struct power_supply *psy = NULL; + struct device *dev; + + power_supply_np = of_parse_phandle(np, property, 0); + if (!power_supply_np) + return ERR_PTR(-ENODEV); + + dev = class_find_device(power_supply_class, NULL, power_supply_np, + power_supply_match_device_node); + + of_node_put(power_supply_np); + + if (dev) { + psy = dev_get_drvdata(dev); + atomic_inc(&psy->use_cnt); + } + + return psy; +} +EXPORT_SYMBOL_GPL(power_supply_get_by_phandle); + +static void devm_power_supply_put(struct device *dev, void *res) +{ + struct power_supply **psy = res; + + power_supply_put(*psy); +} + +/** + * devm_power_supply_get_by_phandle() - Resource managed version of + * power_supply_get_by_phandle() + * @dev: Pointer to device holding phandle property + * @property: Name of property holding a power supply phandle + * + * Return: On success returns a reference to a power supply with + * matching name equals to value under @property, NULL or ERR_PTR otherwise. + */ +struct power_supply *devm_power_supply_get_by_phandle(struct device *dev, + const char *property) +{ + struct power_supply **ptr, *psy; + + if (!dev->of_node) + return ERR_PTR(-ENODEV); + + ptr = devres_alloc(devm_power_supply_put, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + psy = power_supply_get_by_phandle(dev->of_node, property); + if (IS_ERR_OR_NULL(psy)) { + devres_free(ptr); + } else { + *ptr = psy; + devres_add(dev, ptr); + } + return psy; +} +EXPORT_SYMBOL_GPL(devm_power_supply_get_by_phandle); +#endif /* CONFIG_OF */ + +int power_supply_get_battery_info(struct power_supply *psy, + struct power_supply_battery_info *info) +{ + struct power_supply_resistance_temp_table *resist_table; + struct device_node *battery_np; + const char *value; + int err, len, index; + const __be32 *list; + + info->energy_full_design_uwh = -EINVAL; + info->charge_full_design_uah = -EINVAL; + info->voltage_min_design_uv = -EINVAL; + info->voltage_max_design_uv = -EINVAL; + info->precharge_current_ua = -EINVAL; + info->charge_term_current_ua = -EINVAL; + info->constant_charge_current_max_ua = -EINVAL; + info->constant_charge_voltage_max_uv = -EINVAL; + info->temp_ambient_alert_min = INT_MIN; + info->temp_ambient_alert_max = INT_MAX; + info->temp_alert_min = INT_MIN; + info->temp_alert_max = INT_MAX; + info->temp_min = INT_MIN; + info->temp_max = INT_MAX; + info->factory_internal_resistance_uohm = -EINVAL; + info->resist_table = NULL; + + for (index = 0; index < POWER_SUPPLY_OCV_TEMP_MAX; index++) { + info->ocv_table[index] = NULL; + info->ocv_temp[index] = -EINVAL; + info->ocv_table_size[index] = -EINVAL; + } + + if (!psy->of_node) { + dev_warn(&psy->dev, "%s currently only supports devicetree\n", + __func__); + return -ENXIO; + } + + battery_np = of_parse_phandle(psy->of_node, "monitored-battery", 0); + if (!battery_np) + return -ENODEV; + + err = of_property_read_string(battery_np, "compatible", &value); + if (err) + goto out_put_node; + + if (strcmp("simple-battery", value)) { + err = -ENODEV; + goto out_put_node; + } + + /* The property and field names below must correspond to elements + * in enum power_supply_property. For reasoning, see + * Documentation/power/power_supply_class.rst. + */ + + of_property_read_u32(battery_np, "energy-full-design-microwatt-hours", + &info->energy_full_design_uwh); + of_property_read_u32(battery_np, "charge-full-design-microamp-hours", + &info->charge_full_design_uah); + of_property_read_u32(battery_np, "voltage-min-design-microvolt", + &info->voltage_min_design_uv); + of_property_read_u32(battery_np, "voltage-max-design-microvolt", + &info->voltage_max_design_uv); + of_property_read_u32(battery_np, "trickle-charge-current-microamp", + &info->tricklecharge_current_ua); + of_property_read_u32(battery_np, "precharge-current-microamp", + &info->precharge_current_ua); + of_property_read_u32(battery_np, "precharge-upper-limit-microvolt", + &info->precharge_voltage_max_uv); + of_property_read_u32(battery_np, "charge-term-current-microamp", + &info->charge_term_current_ua); + of_property_read_u32(battery_np, "re-charge-voltage-microvolt", + &info->charge_restart_voltage_uv); + of_property_read_u32(battery_np, "over-voltage-threshold-microvolt", + &info->overvoltage_limit_uv); + of_property_read_u32(battery_np, "constant-charge-current-max-microamp", + &info->constant_charge_current_max_ua); + of_property_read_u32(battery_np, "constant-charge-voltage-max-microvolt", + &info->constant_charge_voltage_max_uv); + of_property_read_u32(battery_np, "factory-internal-resistance-micro-ohms", + &info->factory_internal_resistance_uohm); + + of_property_read_u32_index(battery_np, "ambient-celsius", + 0, &info->temp_ambient_alert_min); + of_property_read_u32_index(battery_np, "ambient-celsius", + 1, &info->temp_ambient_alert_max); + of_property_read_u32_index(battery_np, "alert-celsius", + 0, &info->temp_alert_min); + of_property_read_u32_index(battery_np, "alert-celsius", + 1, &info->temp_alert_max); + of_property_read_u32_index(battery_np, "operating-range-celsius", + 0, &info->temp_min); + of_property_read_u32_index(battery_np, "operating-range-celsius", + 1, &info->temp_max); + + len = of_property_count_u32_elems(battery_np, "ocv-capacity-celsius"); + if (len < 0 && len != -EINVAL) { + err = len; + goto out_put_node; + } else if (len > POWER_SUPPLY_OCV_TEMP_MAX) { + dev_err(&psy->dev, "Too many temperature values\n"); + err = -EINVAL; + goto out_put_node; + } else if (len > 0) { + of_property_read_u32_array(battery_np, "ocv-capacity-celsius", + info->ocv_temp, len); + } + + for (index = 0; index < len; index++) { + struct power_supply_battery_ocv_table *table; + char *propname; + int i, tab_len, size; + + propname = kasprintf(GFP_KERNEL, "ocv-capacity-table-%d", index); + if (!propname) { + power_supply_put_battery_info(psy, info); + err = -ENOMEM; + goto out_put_node; + } + list = of_get_property(battery_np, propname, &size); + if (!list || !size) { + dev_err(&psy->dev, "failed to get %s\n", propname); + kfree(propname); + power_supply_put_battery_info(psy, info); + err = -EINVAL; + goto out_put_node; + } + + kfree(propname); + tab_len = size / (2 * sizeof(__be32)); + info->ocv_table_size[index] = tab_len; + + table = info->ocv_table[index] = + devm_kcalloc(&psy->dev, tab_len, sizeof(*table), GFP_KERNEL); + if (!info->ocv_table[index]) { + power_supply_put_battery_info(psy, info); + err = -ENOMEM; + goto out_put_node; + } + + for (i = 0; i < tab_len; i++) { + table[i].ocv = be32_to_cpu(*list); + list++; + table[i].capacity = be32_to_cpu(*list); + list++; + } + } + + list = of_get_property(battery_np, "resistance-temp-table", &len); + if (!list || !len) + goto out_put_node; + + info->resist_table_size = len / (2 * sizeof(__be32)); + resist_table = info->resist_table = devm_kcalloc(&psy->dev, + info->resist_table_size, + sizeof(*resist_table), + GFP_KERNEL); + if (!info->resist_table) { + power_supply_put_battery_info(psy, info); + err = -ENOMEM; + goto out_put_node; + } + + for (index = 0; index < info->resist_table_size; index++) { + resist_table[index].temp = be32_to_cpu(*list++); + resist_table[index].resistance = be32_to_cpu(*list++); + } + +out_put_node: + of_node_put(battery_np); + return err; +} +EXPORT_SYMBOL_GPL(power_supply_get_battery_info); + +void power_supply_put_battery_info(struct power_supply *psy, + struct power_supply_battery_info *info) +{ + int i; + + for (i = 0; i < POWER_SUPPLY_OCV_TEMP_MAX; i++) { + if (info->ocv_table[i]) + devm_kfree(&psy->dev, info->ocv_table[i]); + } + + if (info->resist_table) + devm_kfree(&psy->dev, info->resist_table); +} +EXPORT_SYMBOL_GPL(power_supply_put_battery_info); + +/** + * power_supply_temp2resist_simple() - find the battery internal resistance + * percent + * @table: Pointer to battery resistance temperature table + * @table_len: The table length + * @temp: Current temperature + * + * This helper function is used to look up battery internal resistance percent + * according to current temperature value from the resistance temperature table, + * and the table must be ordered descending. Then the actual battery internal + * resistance = the ideal battery internal resistance * percent / 100. + * + * Return: the battery internal resistance percent + */ +int power_supply_temp2resist_simple(struct power_supply_resistance_temp_table *table, + int table_len, int temp) +{ + int i, resist; + + for (i = 0; i < table_len; i++) + if (temp > table[i].temp) + break; + + if (i > 0 && i < table_len) { + int tmp; + + tmp = (table[i - 1].resistance - table[i].resistance) * + (temp - table[i].temp); + tmp /= table[i - 1].temp - table[i].temp; + resist = tmp + table[i].resistance; + } else if (i == 0) { + resist = table[0].resistance; + } else { + resist = table[table_len - 1].resistance; + } + + return resist; +} +EXPORT_SYMBOL_GPL(power_supply_temp2resist_simple); + +/** + * power_supply_ocv2cap_simple() - find the battery capacity + * @table: Pointer to battery OCV lookup table + * @table_len: OCV table length + * @ocv: Current OCV value + * + * This helper function is used to look up battery capacity according to + * current OCV value from one OCV table, and the OCV table must be ordered + * descending. + * + * Return: the battery capacity. + */ +int power_supply_ocv2cap_simple(struct power_supply_battery_ocv_table *table, + int table_len, int ocv) +{ + int i, cap, tmp; + + for (i = 0; i < table_len; i++) + if (ocv > table[i].ocv) + break; + + if (i > 0 && i < table_len) { + tmp = (table[i - 1].capacity - table[i].capacity) * + (ocv - table[i].ocv); + tmp /= table[i - 1].ocv - table[i].ocv; + cap = tmp + table[i].capacity; + } else if (i == 0) { + cap = table[0].capacity; + } else { + cap = table[table_len - 1].capacity; + } + + return cap; +} +EXPORT_SYMBOL_GPL(power_supply_ocv2cap_simple); + +struct power_supply_battery_ocv_table * +power_supply_find_ocv2cap_table(struct power_supply_battery_info *info, + int temp, int *table_len) +{ + int best_temp_diff = INT_MAX, temp_diff; + u8 i, best_index = 0; + + if (!info->ocv_table[0]) + return NULL; + + for (i = 0; i < POWER_SUPPLY_OCV_TEMP_MAX; i++) { + /* Out of capacity tables */ + if (!info->ocv_table[i]) + break; + + temp_diff = abs(info->ocv_temp[i] - temp); + + if (temp_diff < best_temp_diff) { + best_temp_diff = temp_diff; + best_index = i; + } + } + + *table_len = info->ocv_table_size[best_index]; + return info->ocv_table[best_index]; +} +EXPORT_SYMBOL_GPL(power_supply_find_ocv2cap_table); + +int power_supply_batinfo_ocv2cap(struct power_supply_battery_info *info, + int ocv, int temp) +{ + struct power_supply_battery_ocv_table *table; + int table_len; + + table = power_supply_find_ocv2cap_table(info, temp, &table_len); + if (!table) + return -EINVAL; + + return power_supply_ocv2cap_simple(table, table_len, ocv); +} +EXPORT_SYMBOL_GPL(power_supply_batinfo_ocv2cap); + +int power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + if (atomic_read(&psy->use_cnt) <= 0) { + if (!psy->initialized) + return -EAGAIN; + return -ENODEV; + } + + return psy->desc->get_property(psy, psp, val); +} +EXPORT_SYMBOL_GPL(power_supply_get_property); + +int power_supply_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + if (atomic_read(&psy->use_cnt) <= 0 || !psy->desc->set_property) + return -ENODEV; + + return psy->desc->set_property(psy, psp, val); +} +EXPORT_SYMBOL_GPL(power_supply_set_property); + +int power_supply_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + if (atomic_read(&psy->use_cnt) <= 0 || + !psy->desc->property_is_writeable) + return -ENODEV; + + return psy->desc->property_is_writeable(psy, psp); +} +EXPORT_SYMBOL_GPL(power_supply_property_is_writeable); + +void power_supply_external_power_changed(struct power_supply *psy) +{ + if (atomic_read(&psy->use_cnt) <= 0 || + !psy->desc->external_power_changed) + return; + + psy->desc->external_power_changed(psy); +} +EXPORT_SYMBOL_GPL(power_supply_external_power_changed); + +int power_supply_powers(struct power_supply *psy, struct device *dev) +{ + return sysfs_create_link(&psy->dev.kobj, &dev->kobj, "powers"); +} +EXPORT_SYMBOL_GPL(power_supply_powers); + +static void power_supply_dev_release(struct device *dev) +{ + struct power_supply *psy = to_power_supply(dev); + dev_dbg(dev, "%s\n", __func__); + kfree(psy); +} + +int power_supply_reg_notifier(struct notifier_block *nb) +{ + return atomic_notifier_chain_register(&power_supply_notifier, nb); +} +EXPORT_SYMBOL_GPL(power_supply_reg_notifier); + +void power_supply_unreg_notifier(struct notifier_block *nb) +{ + atomic_notifier_chain_unregister(&power_supply_notifier, nb); +} +EXPORT_SYMBOL_GPL(power_supply_unreg_notifier); + +#ifdef CONFIG_THERMAL +static int power_supply_read_temp(struct thermal_zone_device *tzd, + int *temp) +{ + struct power_supply *psy; + union power_supply_propval val; + int ret; + + WARN_ON(tzd == NULL); + psy = tzd->devdata; + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_TEMP, &val); + if (ret) + return ret; + + /* Convert tenths of degree Celsius to milli degree Celsius. */ + *temp = val.intval * 100; + + return ret; +} + +static struct thermal_zone_device_ops psy_tzd_ops = { + .get_temp = power_supply_read_temp, +}; + +static int psy_register_thermal(struct power_supply *psy) +{ + int i, ret; + + if (psy->desc->no_thermal) + return 0; + + /* Register battery zone device psy reports temperature */ + for (i = 0; i < psy->desc->num_properties; i++) { + if (psy->desc->properties[i] == POWER_SUPPLY_PROP_TEMP) { + psy->tzd = thermal_zone_device_register(psy->desc->name, + 0, 0, psy, &psy_tzd_ops, NULL, 0, 0); + if (IS_ERR(psy->tzd)) + return PTR_ERR(psy->tzd); + ret = thermal_zone_device_enable(psy->tzd); + if (ret) + thermal_zone_device_unregister(psy->tzd); + return ret; + } + } + return 0; +} + +static void psy_unregister_thermal(struct power_supply *psy) +{ + if (IS_ERR_OR_NULL(psy->tzd)) + return; + thermal_zone_device_unregister(psy->tzd); +} + +/* thermal cooling device callbacks */ +static int ps_get_max_charge_cntl_limit(struct thermal_cooling_device *tcd, + unsigned long *state) +{ + struct power_supply *psy; + union power_supply_propval val; + int ret; + + psy = tcd->devdata; + ret = power_supply_get_property(psy, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX, &val); + if (ret) + return ret; + + *state = val.intval; + + return ret; +} + +static int ps_get_cur_charge_cntl_limit(struct thermal_cooling_device *tcd, + unsigned long *state) +{ + struct power_supply *psy; + union power_supply_propval val; + int ret; + + psy = tcd->devdata; + ret = power_supply_get_property(psy, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, &val); + if (ret) + return ret; + + *state = val.intval; + + return ret; +} + +static int ps_set_cur_charge_cntl_limit(struct thermal_cooling_device *tcd, + unsigned long state) +{ + struct power_supply *psy; + union power_supply_propval val; + int ret; + + psy = tcd->devdata; + val.intval = state; + ret = psy->desc->set_property(psy, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, &val); + + return ret; +} + +static const struct thermal_cooling_device_ops psy_tcd_ops = { + .get_max_state = ps_get_max_charge_cntl_limit, + .get_cur_state = ps_get_cur_charge_cntl_limit, + .set_cur_state = ps_set_cur_charge_cntl_limit, +}; + +static int psy_register_cooler(struct power_supply *psy) +{ + int i; + + /* Register for cooling device if psy can control charging */ + for (i = 0; i < psy->desc->num_properties; i++) { + if (psy->desc->properties[i] == + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT) { + psy->tcd = thermal_cooling_device_register( + (char *)psy->desc->name, + psy, &psy_tcd_ops); + return PTR_ERR_OR_ZERO(psy->tcd); + } + } + return 0; +} + +static void psy_unregister_cooler(struct power_supply *psy) +{ + if (IS_ERR_OR_NULL(psy->tcd)) + return; + thermal_cooling_device_unregister(psy->tcd); +} +#else +static int psy_register_thermal(struct power_supply *psy) +{ + return 0; +} + +static void psy_unregister_thermal(struct power_supply *psy) +{ +} + +static int psy_register_cooler(struct power_supply *psy) +{ + return 0; +} + +static void psy_unregister_cooler(struct power_supply *psy) +{ +} +#endif + +static struct power_supply *__must_check +__power_supply_register(struct device *parent, + const struct power_supply_desc *desc, + const struct power_supply_config *cfg, + bool ws) +{ + struct device *dev; + struct power_supply *psy; + int i, rc; + + if (!parent) + pr_warn("%s: Expected proper parent device for '%s'\n", + __func__, desc->name); + + if (!desc || !desc->name || !desc->properties || !desc->num_properties) + return ERR_PTR(-EINVAL); + + for (i = 0; i < desc->num_properties; ++i) { + if ((desc->properties[i] == POWER_SUPPLY_PROP_USB_TYPE) && + (!desc->usb_types || !desc->num_usb_types)) + return ERR_PTR(-EINVAL); + } + + psy = kzalloc(sizeof(*psy), GFP_KERNEL); + if (!psy) + return ERR_PTR(-ENOMEM); + + dev = &psy->dev; + + device_initialize(dev); + + dev->class = power_supply_class; + dev->type = &power_supply_dev_type; + dev->parent = parent; + dev->release = power_supply_dev_release; + dev_set_drvdata(dev, psy); + psy->desc = desc; + if (cfg) { + dev->groups = cfg->attr_grp; + psy->drv_data = cfg->drv_data; + psy->of_node = + cfg->fwnode ? to_of_node(cfg->fwnode) : cfg->of_node; + psy->supplied_to = cfg->supplied_to; + psy->num_supplicants = cfg->num_supplicants; + } + + rc = dev_set_name(dev, "%s", desc->name); + if (rc) + goto dev_set_name_failed; + + INIT_WORK(&psy->changed_work, power_supply_changed_work); + INIT_DELAYED_WORK(&psy->deferred_register_work, + power_supply_deferred_register_work); + + rc = power_supply_check_supplies(psy); + if (rc) { + dev_info(dev, "Not all required supplies found, defer probe\n"); + goto check_supplies_failed; + } + + spin_lock_init(&psy->changed_lock); + rc = device_add(dev); + if (rc) + goto device_add_failed; + + rc = device_init_wakeup(dev, ws); + if (rc) + goto wakeup_init_failed; + + rc = psy_register_thermal(psy); + if (rc) + goto register_thermal_failed; + + rc = psy_register_cooler(psy); + if (rc) + goto register_cooler_failed; + + rc = power_supply_create_triggers(psy); + if (rc) + goto create_triggers_failed; + + rc = power_supply_add_hwmon_sysfs(psy); + if (rc) + goto add_hwmon_sysfs_failed; + + /* + * Update use_cnt after any uevents (most notably from device_add()). + * We are here still during driver's probe but + * the power_supply_uevent() calls back driver's get_property + * method so: + * 1. Driver did not assigned the returned struct power_supply, + * 2. Driver could not finish initialization (anything in its probe + * after calling power_supply_register()). + */ + atomic_inc(&psy->use_cnt); + psy->initialized = true; + + queue_delayed_work(system_power_efficient_wq, + &psy->deferred_register_work, + POWER_SUPPLY_DEFERRED_REGISTER_TIME); + + return psy; + +add_hwmon_sysfs_failed: + power_supply_remove_triggers(psy); +create_triggers_failed: + psy_unregister_cooler(psy); +register_cooler_failed: + psy_unregister_thermal(psy); +register_thermal_failed: +wakeup_init_failed: + device_del(dev); +device_add_failed: +check_supplies_failed: +dev_set_name_failed: + put_device(dev); + return ERR_PTR(rc); +} + +/** + * power_supply_register() - Register new power supply + * @parent: Device to be a parent of power supply's device, usually + * the device which probe function calls this + * @desc: Description of power supply, must be valid through whole + * lifetime of this power supply + * @cfg: Run-time specific configuration accessed during registering, + * may be NULL + * + * Return: A pointer to newly allocated power_supply on success + * or ERR_PTR otherwise. + * Use power_supply_unregister() on returned power_supply pointer to release + * resources. + */ +struct power_supply *__must_check power_supply_register(struct device *parent, + const struct power_supply_desc *desc, + const struct power_supply_config *cfg) +{ + return __power_supply_register(parent, desc, cfg, true); +} +EXPORT_SYMBOL_GPL(power_supply_register); + +/** + * power_supply_register_no_ws() - Register new non-waking-source power supply + * @parent: Device to be a parent of power supply's device, usually + * the device which probe function calls this + * @desc: Description of power supply, must be valid through whole + * lifetime of this power supply + * @cfg: Run-time specific configuration accessed during registering, + * may be NULL + * + * Return: A pointer to newly allocated power_supply on success + * or ERR_PTR otherwise. + * Use power_supply_unregister() on returned power_supply pointer to release + * resources. + */ +struct power_supply *__must_check +power_supply_register_no_ws(struct device *parent, + const struct power_supply_desc *desc, + const struct power_supply_config *cfg) +{ + return __power_supply_register(parent, desc, cfg, false); +} +EXPORT_SYMBOL_GPL(power_supply_register_no_ws); + +static void devm_power_supply_release(struct device *dev, void *res) +{ + struct power_supply **psy = res; + + power_supply_unregister(*psy); +} + +/** + * devm_power_supply_register() - Register managed power supply + * @parent: Device to be a parent of power supply's device, usually + * the device which probe function calls this + * @desc: Description of power supply, must be valid through whole + * lifetime of this power supply + * @cfg: Run-time specific configuration accessed during registering, + * may be NULL + * + * Return: A pointer to newly allocated power_supply on success + * or ERR_PTR otherwise. + * The returned power_supply pointer will be automatically unregistered + * on driver detach. + */ +struct power_supply *__must_check +devm_power_supply_register(struct device *parent, + const struct power_supply_desc *desc, + const struct power_supply_config *cfg) +{ + struct power_supply **ptr, *psy; + + ptr = devres_alloc(devm_power_supply_release, sizeof(*ptr), GFP_KERNEL); + + if (!ptr) + return ERR_PTR(-ENOMEM); + psy = __power_supply_register(parent, desc, cfg, true); + if (IS_ERR(psy)) { + devres_free(ptr); + } else { + *ptr = psy; + devres_add(parent, ptr); + } + return psy; +} +EXPORT_SYMBOL_GPL(devm_power_supply_register); + +/** + * devm_power_supply_register_no_ws() - Register managed non-waking-source power supply + * @parent: Device to be a parent of power supply's device, usually + * the device which probe function calls this + * @desc: Description of power supply, must be valid through whole + * lifetime of this power supply + * @cfg: Run-time specific configuration accessed during registering, + * may be NULL + * + * Return: A pointer to newly allocated power_supply on success + * or ERR_PTR otherwise. + * The returned power_supply pointer will be automatically unregistered + * on driver detach. + */ +struct power_supply *__must_check +devm_power_supply_register_no_ws(struct device *parent, + const struct power_supply_desc *desc, + const struct power_supply_config *cfg) +{ + struct power_supply **ptr, *psy; + + ptr = devres_alloc(devm_power_supply_release, sizeof(*ptr), GFP_KERNEL); + + if (!ptr) + return ERR_PTR(-ENOMEM); + psy = __power_supply_register(parent, desc, cfg, false); + if (IS_ERR(psy)) { + devres_free(ptr); + } else { + *ptr = psy; + devres_add(parent, ptr); + } + return psy; +} +EXPORT_SYMBOL_GPL(devm_power_supply_register_no_ws); + +/** + * power_supply_unregister() - Remove this power supply from system + * @psy: Pointer to power supply to unregister + * + * Remove this power supply from the system. The resources of power supply + * will be freed here or on last power_supply_put() call. + */ +void power_supply_unregister(struct power_supply *psy) +{ + WARN_ON(atomic_dec_return(&psy->use_cnt)); + psy->removing = true; + cancel_work_sync(&psy->changed_work); + cancel_delayed_work_sync(&psy->deferred_register_work); + sysfs_remove_link(&psy->dev.kobj, "powers"); + power_supply_remove_hwmon_sysfs(psy); + power_supply_remove_triggers(psy); + psy_unregister_cooler(psy); + psy_unregister_thermal(psy); + device_init_wakeup(&psy->dev, false); + device_unregister(&psy->dev); +} +EXPORT_SYMBOL_GPL(power_supply_unregister); + +void *power_supply_get_drvdata(struct power_supply *psy) +{ + return psy->drv_data; +} +EXPORT_SYMBOL_GPL(power_supply_get_drvdata); + +static int __init power_supply_class_init(void) +{ + power_supply_class = class_create(THIS_MODULE, "power_supply"); + + if (IS_ERR(power_supply_class)) + return PTR_ERR(power_supply_class); + + power_supply_class->dev_uevent = power_supply_uevent; + power_supply_init_attrs(&power_supply_dev_type); + + return 0; +} + +static void __exit power_supply_class_exit(void) +{ + class_destroy(power_supply_class); +} + +subsys_initcall(power_supply_class_init); +module_exit(power_supply_class_exit); + +MODULE_DESCRIPTION("Universal power supply monitor class"); +MODULE_AUTHOR("Ian Molton <spyro@f2s.com>, " + "Szabolcs Gyurko, " + "Anton Vorontsov <cbou@mail.ru>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/power_supply_hwmon.c b/drivers/power/supply/power_supply_hwmon.c new file mode 100644 index 000000000..7fe4b6b6d --- /dev/null +++ b/drivers/power/supply/power_supply_hwmon.c @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * power_supply_hwmon.c - power supply hwmon support. + */ + +#include <linux/err.h> +#include <linux/hwmon.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +struct power_supply_hwmon { + struct power_supply *psy; + unsigned long *props; +}; + +static const char *const ps_temp_label[] = { + "temp", + "ambient temp", +}; + +static int power_supply_hwmon_in_to_property(u32 attr) +{ + switch (attr) { + case hwmon_in_average: + return POWER_SUPPLY_PROP_VOLTAGE_AVG; + case hwmon_in_min: + return POWER_SUPPLY_PROP_VOLTAGE_MIN; + case hwmon_in_max: + return POWER_SUPPLY_PROP_VOLTAGE_MAX; + case hwmon_in_input: + return POWER_SUPPLY_PROP_VOLTAGE_NOW; + default: + return -EINVAL; + } +} + +static int power_supply_hwmon_curr_to_property(u32 attr) +{ + switch (attr) { + case hwmon_curr_average: + return POWER_SUPPLY_PROP_CURRENT_AVG; + case hwmon_curr_max: + return POWER_SUPPLY_PROP_CURRENT_MAX; + case hwmon_curr_input: + return POWER_SUPPLY_PROP_CURRENT_NOW; + default: + return -EINVAL; + } +} + +static int power_supply_hwmon_temp_to_property(u32 attr, int channel) +{ + if (channel) { + switch (attr) { + case hwmon_temp_input: + return POWER_SUPPLY_PROP_TEMP_AMBIENT; + case hwmon_temp_min_alarm: + return POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MIN; + case hwmon_temp_max_alarm: + return POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MAX; + default: + break; + } + } else { + switch (attr) { + case hwmon_temp_input: + return POWER_SUPPLY_PROP_TEMP; + case hwmon_temp_max: + return POWER_SUPPLY_PROP_TEMP_MAX; + case hwmon_temp_min: + return POWER_SUPPLY_PROP_TEMP_MIN; + case hwmon_temp_min_alarm: + return POWER_SUPPLY_PROP_TEMP_ALERT_MIN; + case hwmon_temp_max_alarm: + return POWER_SUPPLY_PROP_TEMP_ALERT_MAX; + default: + break; + } + } + + return -EINVAL; +} + +static int +power_supply_hwmon_to_property(enum hwmon_sensor_types type, + u32 attr, int channel) +{ + switch (type) { + case hwmon_in: + return power_supply_hwmon_in_to_property(attr); + case hwmon_curr: + return power_supply_hwmon_curr_to_property(attr); + case hwmon_temp: + return power_supply_hwmon_temp_to_property(attr, channel); + default: + return -EINVAL; + } +} + +static bool power_supply_hwmon_is_a_label(enum hwmon_sensor_types type, + u32 attr) +{ + return type == hwmon_temp && attr == hwmon_temp_label; +} + +struct hwmon_type_attr_list { + const u32 *attrs; + size_t n_attrs; +}; + +static const u32 ps_temp_attrs[] = { + hwmon_temp_input, + hwmon_temp_min, hwmon_temp_max, + hwmon_temp_min_alarm, hwmon_temp_max_alarm, +}; + +static const struct hwmon_type_attr_list ps_type_attrs[hwmon_max] = { + [hwmon_temp] = { ps_temp_attrs, ARRAY_SIZE(ps_temp_attrs) }, +}; + +static bool power_supply_hwmon_has_input( + const struct power_supply_hwmon *psyhw, + enum hwmon_sensor_types type, int channel) +{ + const struct hwmon_type_attr_list *attr_list = &ps_type_attrs[type]; + size_t i; + + for (i = 0; i < attr_list->n_attrs; ++i) { + int prop = power_supply_hwmon_to_property(type, + attr_list->attrs[i], channel); + + if (prop >= 0 && test_bit(prop, psyhw->props)) + return true; + } + + return false; +} + +static bool power_supply_hwmon_is_writable(enum hwmon_sensor_types type, + u32 attr) +{ + switch (type) { + case hwmon_in: + return attr == hwmon_in_min || + attr == hwmon_in_max; + case hwmon_curr: + return attr == hwmon_curr_max; + case hwmon_temp: + return attr == hwmon_temp_max || + attr == hwmon_temp_min || + attr == hwmon_temp_min_alarm || + attr == hwmon_temp_max_alarm; + default: + return false; + } +} + +static umode_t power_supply_hwmon_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct power_supply_hwmon *psyhw = data; + int prop; + + if (power_supply_hwmon_is_a_label(type, attr)) { + if (power_supply_hwmon_has_input(psyhw, type, channel)) + return 0444; + else + return 0; + } + + prop = power_supply_hwmon_to_property(type, attr, channel); + if (prop < 0 || !test_bit(prop, psyhw->props)) + return 0; + + if (power_supply_property_is_writeable(psyhw->psy, prop) > 0 && + power_supply_hwmon_is_writable(type, attr)) + return 0644; + + return 0444; +} + +static int power_supply_hwmon_read_string(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, int channel, + const char **str) +{ + switch (type) { + case hwmon_temp: + *str = ps_temp_label[channel]; + break; + default: + /* unreachable, but see: + * gcc bug #51513 [1] and clang bug #978 [2] + * + * [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51513 + * [2] https://github.com/ClangBuiltLinux/linux/issues/978 + */ + break; + } + + return 0; +} + +static int +power_supply_hwmon_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct power_supply_hwmon *psyhw = dev_get_drvdata(dev); + struct power_supply *psy = psyhw->psy; + union power_supply_propval pspval; + int ret, prop; + + prop = power_supply_hwmon_to_property(type, attr, channel); + if (prop < 0) + return prop; + + ret = power_supply_get_property(psy, prop, &pspval); + if (ret) + return ret; + + switch (type) { + /* + * Both voltage and current is reported in units of + * microvolts/microamps, so we need to adjust it to + * milliamps(volts) + */ + case hwmon_curr: + case hwmon_in: + pspval.intval = DIV_ROUND_CLOSEST(pspval.intval, 1000); + break; + /* + * Temp needs to be converted from 1/10 C to milli-C + */ + case hwmon_temp: + if (check_mul_overflow(pspval.intval, 100, + &pspval.intval)) + return -EOVERFLOW; + break; + default: + return -EINVAL; + } + + *val = pspval.intval; + + return 0; +} + +static int +power_supply_hwmon_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + struct power_supply_hwmon *psyhw = dev_get_drvdata(dev); + struct power_supply *psy = psyhw->psy; + union power_supply_propval pspval; + int prop; + + prop = power_supply_hwmon_to_property(type, attr, channel); + if (prop < 0) + return prop; + + pspval.intval = val; + + switch (type) { + /* + * Both voltage and current is reported in units of + * microvolts/microamps, so we need to adjust it to + * milliamps(volts) + */ + case hwmon_curr: + case hwmon_in: + if (check_mul_overflow(pspval.intval, 1000, + &pspval.intval)) + return -EOVERFLOW; + break; + /* + * Temp needs to be converted from 1/10 C to milli-C + */ + case hwmon_temp: + pspval.intval = DIV_ROUND_CLOSEST(pspval.intval, 100); + break; + default: + return -EINVAL; + } + + return power_supply_set_property(psy, prop, &pspval); +} + +static const struct hwmon_ops power_supply_hwmon_ops = { + .is_visible = power_supply_hwmon_is_visible, + .read = power_supply_hwmon_read, + .write = power_supply_hwmon_write, + .read_string = power_supply_hwmon_read_string, +}; + +static const struct hwmon_channel_info *power_supply_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_LABEL | + HWMON_T_INPUT | + HWMON_T_MAX | + HWMON_T_MIN | + HWMON_T_MIN_ALARM | + HWMON_T_MIN_ALARM, + + HWMON_T_LABEL | + HWMON_T_INPUT | + HWMON_T_MIN_ALARM | + HWMON_T_LABEL | + HWMON_T_MAX_ALARM), + + HWMON_CHANNEL_INFO(curr, + HWMON_C_AVERAGE | + HWMON_C_MAX | + HWMON_C_INPUT), + + HWMON_CHANNEL_INFO(in, + HWMON_I_AVERAGE | + HWMON_I_MIN | + HWMON_I_MAX | + HWMON_I_INPUT), + NULL +}; + +static const struct hwmon_chip_info power_supply_hwmon_chip_info = { + .ops = &power_supply_hwmon_ops, + .info = power_supply_hwmon_info, +}; + +static void power_supply_hwmon_bitmap_free(void *data) +{ + bitmap_free(data); +} + +int power_supply_add_hwmon_sysfs(struct power_supply *psy) +{ + const struct power_supply_desc *desc = psy->desc; + struct power_supply_hwmon *psyhw; + struct device *dev = &psy->dev; + struct device *hwmon; + int ret, i; + const char *name; + + if (!devres_open_group(dev, power_supply_add_hwmon_sysfs, + GFP_KERNEL)) + return -ENOMEM; + + psyhw = devm_kzalloc(dev, sizeof(*psyhw), GFP_KERNEL); + if (!psyhw) { + ret = -ENOMEM; + goto error; + } + + psyhw->psy = psy; + psyhw->props = bitmap_zalloc(POWER_SUPPLY_PROP_TIME_TO_FULL_AVG + 1, + GFP_KERNEL); + if (!psyhw->props) { + ret = -ENOMEM; + goto error; + } + + ret = devm_add_action_or_reset(dev, power_supply_hwmon_bitmap_free, + psyhw->props); + if (ret) + goto error; + + for (i = 0; i < desc->num_properties; i++) { + const enum power_supply_property prop = desc->properties[i]; + + switch (prop) { + case POWER_SUPPLY_PROP_CURRENT_AVG: + case POWER_SUPPLY_PROP_CURRENT_MAX: + case POWER_SUPPLY_PROP_CURRENT_NOW: + case POWER_SUPPLY_PROP_TEMP: + case POWER_SUPPLY_PROP_TEMP_MAX: + case POWER_SUPPLY_PROP_TEMP_MIN: + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + case POWER_SUPPLY_PROP_TEMP_AMBIENT: + case POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MIN: + case POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MAX: + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + set_bit(prop, psyhw->props); + break; + default: + break; + } + } + + name = psy->desc->name; + if (strchr(name, '-')) { + char *new_name; + + new_name = devm_kstrdup(dev, name, GFP_KERNEL); + if (!new_name) { + ret = -ENOMEM; + goto error; + } + strreplace(new_name, '-', '_'); + name = new_name; + } + hwmon = devm_hwmon_device_register_with_info(dev, name, + psyhw, + &power_supply_hwmon_chip_info, + NULL); + ret = PTR_ERR_OR_ZERO(hwmon); + if (ret) + goto error; + + devres_close_group(dev, power_supply_add_hwmon_sysfs); + return 0; +error: + devres_release_group(dev, NULL); + return ret; +} + +void power_supply_remove_hwmon_sysfs(struct power_supply *psy) +{ + devres_release_group(&psy->dev, power_supply_add_hwmon_sysfs); +} diff --git a/drivers/power/supply/power_supply_leds.c b/drivers/power/supply/power_supply_leds.c new file mode 100644 index 000000000..b7a2778f8 --- /dev/null +++ b/drivers/power/supply/power_supply_leds.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs triggers for power supply class + * + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + * Copyright © 2004 Szabolcs Gyurko + * Copyright © 2003 Ian Molton <spyro@f2s.com> + * + * Modified: 2004, Oct Szabolcs Gyurko + */ + +#include <linux/kernel.h> +#include <linux/device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#include "power_supply.h" + +/* Battery specific LEDs triggers. */ + +static void power_supply_update_bat_leds(struct power_supply *psy) +{ + union power_supply_propval status; + unsigned long delay_on = 0; + unsigned long delay_off = 0; + + if (power_supply_get_property(psy, POWER_SUPPLY_PROP_STATUS, &status)) + return; + + dev_dbg(&psy->dev, "%s %d\n", __func__, status.intval); + + switch (status.intval) { + case POWER_SUPPLY_STATUS_FULL: + led_trigger_event(psy->charging_full_trig, LED_FULL); + led_trigger_event(psy->charging_trig, LED_OFF); + led_trigger_event(psy->full_trig, LED_FULL); + /* Going from blink to LED on requires a LED_OFF event to stop blink */ + led_trigger_event(psy->charging_blink_full_solid_trig, LED_OFF); + led_trigger_event(psy->charging_blink_full_solid_trig, LED_FULL); + break; + case POWER_SUPPLY_STATUS_CHARGING: + led_trigger_event(psy->charging_full_trig, LED_FULL); + led_trigger_event(psy->charging_trig, LED_FULL); + led_trigger_event(psy->full_trig, LED_OFF); + led_trigger_blink(psy->charging_blink_full_solid_trig, + &delay_on, &delay_off); + break; + default: + led_trigger_event(psy->charging_full_trig, LED_OFF); + led_trigger_event(psy->charging_trig, LED_OFF); + led_trigger_event(psy->full_trig, LED_OFF); + led_trigger_event(psy->charging_blink_full_solid_trig, + LED_OFF); + break; + } +} + +static int power_supply_create_bat_triggers(struct power_supply *psy) +{ + psy->charging_full_trig_name = kasprintf(GFP_KERNEL, + "%s-charging-or-full", psy->desc->name); + if (!psy->charging_full_trig_name) + goto charging_full_failed; + + psy->charging_trig_name = kasprintf(GFP_KERNEL, + "%s-charging", psy->desc->name); + if (!psy->charging_trig_name) + goto charging_failed; + + psy->full_trig_name = kasprintf(GFP_KERNEL, "%s-full", psy->desc->name); + if (!psy->full_trig_name) + goto full_failed; + + psy->charging_blink_full_solid_trig_name = kasprintf(GFP_KERNEL, + "%s-charging-blink-full-solid", psy->desc->name); + if (!psy->charging_blink_full_solid_trig_name) + goto charging_blink_full_solid_failed; + + led_trigger_register_simple(psy->charging_full_trig_name, + &psy->charging_full_trig); + led_trigger_register_simple(psy->charging_trig_name, + &psy->charging_trig); + led_trigger_register_simple(psy->full_trig_name, + &psy->full_trig); + led_trigger_register_simple(psy->charging_blink_full_solid_trig_name, + &psy->charging_blink_full_solid_trig); + + return 0; + +charging_blink_full_solid_failed: + kfree(psy->full_trig_name); +full_failed: + kfree(psy->charging_trig_name); +charging_failed: + kfree(psy->charging_full_trig_name); +charging_full_failed: + return -ENOMEM; +} + +static void power_supply_remove_bat_triggers(struct power_supply *psy) +{ + led_trigger_unregister_simple(psy->charging_full_trig); + led_trigger_unregister_simple(psy->charging_trig); + led_trigger_unregister_simple(psy->full_trig); + led_trigger_unregister_simple(psy->charging_blink_full_solid_trig); + kfree(psy->charging_blink_full_solid_trig_name); + kfree(psy->full_trig_name); + kfree(psy->charging_trig_name); + kfree(psy->charging_full_trig_name); +} + +/* Generated power specific LEDs triggers. */ + +static void power_supply_update_gen_leds(struct power_supply *psy) +{ + union power_supply_propval online; + + if (power_supply_get_property(psy, POWER_SUPPLY_PROP_ONLINE, &online)) + return; + + dev_dbg(&psy->dev, "%s %d\n", __func__, online.intval); + + if (online.intval) + led_trigger_event(psy->online_trig, LED_FULL); + else + led_trigger_event(psy->online_trig, LED_OFF); +} + +static int power_supply_create_gen_triggers(struct power_supply *psy) +{ + psy->online_trig_name = kasprintf(GFP_KERNEL, "%s-online", + psy->desc->name); + if (!psy->online_trig_name) + return -ENOMEM; + + led_trigger_register_simple(psy->online_trig_name, &psy->online_trig); + + return 0; +} + +static void power_supply_remove_gen_triggers(struct power_supply *psy) +{ + led_trigger_unregister_simple(psy->online_trig); + kfree(psy->online_trig_name); +} + +/* Choice what triggers to create&update. */ + +void power_supply_update_leds(struct power_supply *psy) +{ + if (psy->desc->type == POWER_SUPPLY_TYPE_BATTERY) + power_supply_update_bat_leds(psy); + else + power_supply_update_gen_leds(psy); +} + +int power_supply_create_triggers(struct power_supply *psy) +{ + if (psy->desc->type == POWER_SUPPLY_TYPE_BATTERY) + return power_supply_create_bat_triggers(psy); + return power_supply_create_gen_triggers(psy); +} + +void power_supply_remove_triggers(struct power_supply *psy) +{ + if (psy->desc->type == POWER_SUPPLY_TYPE_BATTERY) + power_supply_remove_bat_triggers(psy); + else + power_supply_remove_gen_triggers(psy); +} diff --git a/drivers/power/supply/power_supply_sysfs.c b/drivers/power/supply/power_supply_sysfs.c new file mode 100644 index 000000000..2b1df9c33 --- /dev/null +++ b/drivers/power/supply/power_supply_sysfs.c @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Sysfs interface for the universal power supply monitor class + * + * Copyright © 2007 David Woodhouse <dwmw2@infradead.org> + * Copyright © 2007 Anton Vorontsov <cbou@mail.ru> + * Copyright © 2004 Szabolcs Gyurko + * Copyright © 2003 Ian Molton <spyro@f2s.com> + * + * Modified: 2004, Oct Szabolcs Gyurko + */ + +#include <linux/ctype.h> +#include <linux/device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/stat.h> + +#include "power_supply.h" + +#define MAX_PROP_NAME_LEN 30 + +struct power_supply_attr { + const char *prop_name; + char attr_name[MAX_PROP_NAME_LEN + 1]; + struct device_attribute dev_attr; + const char * const *text_values; + int text_values_len; +}; + +#define _POWER_SUPPLY_ATTR(_name, _text, _len) \ +[POWER_SUPPLY_PROP_ ## _name] = \ +{ \ + .prop_name = #_name, \ + .attr_name = #_name "\0", \ + .text_values = _text, \ + .text_values_len = _len, \ +} + +#define POWER_SUPPLY_ATTR(_name) _POWER_SUPPLY_ATTR(_name, NULL, 0) +#define _POWER_SUPPLY_ENUM_ATTR(_name, _text) \ + _POWER_SUPPLY_ATTR(_name, _text, ARRAY_SIZE(_text)) +#define POWER_SUPPLY_ENUM_ATTR(_name) \ + _POWER_SUPPLY_ENUM_ATTR(_name, POWER_SUPPLY_ ## _name ## _TEXT) + +static const char * const POWER_SUPPLY_TYPE_TEXT[] = { + [POWER_SUPPLY_TYPE_UNKNOWN] = "Unknown", + [POWER_SUPPLY_TYPE_BATTERY] = "Battery", + [POWER_SUPPLY_TYPE_UPS] = "UPS", + [POWER_SUPPLY_TYPE_MAINS] = "Mains", + [POWER_SUPPLY_TYPE_USB] = "USB", + [POWER_SUPPLY_TYPE_USB_DCP] = "USB_DCP", + [POWER_SUPPLY_TYPE_USB_CDP] = "USB_CDP", + [POWER_SUPPLY_TYPE_USB_ACA] = "USB_ACA", + [POWER_SUPPLY_TYPE_USB_TYPE_C] = "USB_C", + [POWER_SUPPLY_TYPE_USB_PD] = "USB_PD", + [POWER_SUPPLY_TYPE_USB_PD_DRP] = "USB_PD_DRP", + [POWER_SUPPLY_TYPE_APPLE_BRICK_ID] = "BrickID", + [POWER_SUPPLY_TYPE_WIRELESS] = "Wireless", +}; + +static const char * const POWER_SUPPLY_USB_TYPE_TEXT[] = { + [POWER_SUPPLY_USB_TYPE_UNKNOWN] = "Unknown", + [POWER_SUPPLY_USB_TYPE_SDP] = "SDP", + [POWER_SUPPLY_USB_TYPE_DCP] = "DCP", + [POWER_SUPPLY_USB_TYPE_CDP] = "CDP", + [POWER_SUPPLY_USB_TYPE_ACA] = "ACA", + [POWER_SUPPLY_USB_TYPE_C] = "C", + [POWER_SUPPLY_USB_TYPE_PD] = "PD", + [POWER_SUPPLY_USB_TYPE_PD_DRP] = "PD_DRP", + [POWER_SUPPLY_USB_TYPE_PD_PPS] = "PD_PPS", + [POWER_SUPPLY_USB_TYPE_APPLE_BRICK_ID] = "BrickID", +}; + +static const char * const POWER_SUPPLY_STATUS_TEXT[] = { + [POWER_SUPPLY_STATUS_UNKNOWN] = "Unknown", + [POWER_SUPPLY_STATUS_CHARGING] = "Charging", + [POWER_SUPPLY_STATUS_DISCHARGING] = "Discharging", + [POWER_SUPPLY_STATUS_NOT_CHARGING] = "Not charging", + [POWER_SUPPLY_STATUS_FULL] = "Full", +}; + +static const char * const POWER_SUPPLY_CHARGE_TYPE_TEXT[] = { + [POWER_SUPPLY_CHARGE_TYPE_UNKNOWN] = "Unknown", + [POWER_SUPPLY_CHARGE_TYPE_NONE] = "N/A", + [POWER_SUPPLY_CHARGE_TYPE_TRICKLE] = "Trickle", + [POWER_SUPPLY_CHARGE_TYPE_FAST] = "Fast", + [POWER_SUPPLY_CHARGE_TYPE_STANDARD] = "Standard", + [POWER_SUPPLY_CHARGE_TYPE_ADAPTIVE] = "Adaptive", + [POWER_SUPPLY_CHARGE_TYPE_CUSTOM] = "Custom", + [POWER_SUPPLY_CHARGE_TYPE_LONGLIFE] = "Long Life", +}; + +static const char * const POWER_SUPPLY_HEALTH_TEXT[] = { + [POWER_SUPPLY_HEALTH_UNKNOWN] = "Unknown", + [POWER_SUPPLY_HEALTH_GOOD] = "Good", + [POWER_SUPPLY_HEALTH_OVERHEAT] = "Overheat", + [POWER_SUPPLY_HEALTH_DEAD] = "Dead", + [POWER_SUPPLY_HEALTH_OVERVOLTAGE] = "Over voltage", + [POWER_SUPPLY_HEALTH_UNSPEC_FAILURE] = "Unspecified failure", + [POWER_SUPPLY_HEALTH_COLD] = "Cold", + [POWER_SUPPLY_HEALTH_WATCHDOG_TIMER_EXPIRE] = "Watchdog timer expire", + [POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE] = "Safety timer expire", + [POWER_SUPPLY_HEALTH_OVERCURRENT] = "Over current", + [POWER_SUPPLY_HEALTH_CALIBRATION_REQUIRED] = "Calibration required", + [POWER_SUPPLY_HEALTH_WARM] = "Warm", + [POWER_SUPPLY_HEALTH_COOL] = "Cool", + [POWER_SUPPLY_HEALTH_HOT] = "Hot", +}; + +static const char * const POWER_SUPPLY_TECHNOLOGY_TEXT[] = { + [POWER_SUPPLY_TECHNOLOGY_UNKNOWN] = "Unknown", + [POWER_SUPPLY_TECHNOLOGY_NiMH] = "NiMH", + [POWER_SUPPLY_TECHNOLOGY_LION] = "Li-ion", + [POWER_SUPPLY_TECHNOLOGY_LIPO] = "Li-poly", + [POWER_SUPPLY_TECHNOLOGY_LiFe] = "LiFe", + [POWER_SUPPLY_TECHNOLOGY_NiCd] = "NiCd", + [POWER_SUPPLY_TECHNOLOGY_LiMn] = "LiMn", +}; + +static const char * const POWER_SUPPLY_CAPACITY_LEVEL_TEXT[] = { + [POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN] = "Unknown", + [POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL] = "Critical", + [POWER_SUPPLY_CAPACITY_LEVEL_LOW] = "Low", + [POWER_SUPPLY_CAPACITY_LEVEL_NORMAL] = "Normal", + [POWER_SUPPLY_CAPACITY_LEVEL_HIGH] = "High", + [POWER_SUPPLY_CAPACITY_LEVEL_FULL] = "Full", +}; + +static const char * const POWER_SUPPLY_SCOPE_TEXT[] = { + [POWER_SUPPLY_SCOPE_UNKNOWN] = "Unknown", + [POWER_SUPPLY_SCOPE_SYSTEM] = "System", + [POWER_SUPPLY_SCOPE_DEVICE] = "Device", +}; + +static struct power_supply_attr power_supply_attrs[] = { + /* Properties of type `int' */ + POWER_SUPPLY_ENUM_ATTR(STATUS), + POWER_SUPPLY_ENUM_ATTR(CHARGE_TYPE), + POWER_SUPPLY_ENUM_ATTR(HEALTH), + POWER_SUPPLY_ATTR(PRESENT), + POWER_SUPPLY_ATTR(ONLINE), + POWER_SUPPLY_ATTR(AUTHENTIC), + POWER_SUPPLY_ENUM_ATTR(TECHNOLOGY), + POWER_SUPPLY_ATTR(CYCLE_COUNT), + POWER_SUPPLY_ATTR(VOLTAGE_MAX), + POWER_SUPPLY_ATTR(VOLTAGE_MIN), + POWER_SUPPLY_ATTR(VOLTAGE_MAX_DESIGN), + POWER_SUPPLY_ATTR(VOLTAGE_MIN_DESIGN), + POWER_SUPPLY_ATTR(VOLTAGE_NOW), + POWER_SUPPLY_ATTR(VOLTAGE_AVG), + POWER_SUPPLY_ATTR(VOLTAGE_OCV), + POWER_SUPPLY_ATTR(VOLTAGE_BOOT), + POWER_SUPPLY_ATTR(CURRENT_MAX), + POWER_SUPPLY_ATTR(CURRENT_NOW), + POWER_SUPPLY_ATTR(CURRENT_AVG), + POWER_SUPPLY_ATTR(CURRENT_BOOT), + POWER_SUPPLY_ATTR(POWER_NOW), + POWER_SUPPLY_ATTR(POWER_AVG), + POWER_SUPPLY_ATTR(CHARGE_FULL_DESIGN), + POWER_SUPPLY_ATTR(CHARGE_EMPTY_DESIGN), + POWER_SUPPLY_ATTR(CHARGE_FULL), + POWER_SUPPLY_ATTR(CHARGE_EMPTY), + POWER_SUPPLY_ATTR(CHARGE_NOW), + POWER_SUPPLY_ATTR(CHARGE_AVG), + POWER_SUPPLY_ATTR(CHARGE_COUNTER), + POWER_SUPPLY_ATTR(CONSTANT_CHARGE_CURRENT), + POWER_SUPPLY_ATTR(CONSTANT_CHARGE_CURRENT_MAX), + POWER_SUPPLY_ATTR(CONSTANT_CHARGE_VOLTAGE), + POWER_SUPPLY_ATTR(CONSTANT_CHARGE_VOLTAGE_MAX), + POWER_SUPPLY_ATTR(CHARGE_CONTROL_LIMIT), + POWER_SUPPLY_ATTR(CHARGE_CONTROL_LIMIT_MAX), + POWER_SUPPLY_ATTR(CHARGE_CONTROL_START_THRESHOLD), + POWER_SUPPLY_ATTR(CHARGE_CONTROL_END_THRESHOLD), + POWER_SUPPLY_ATTR(INPUT_CURRENT_LIMIT), + POWER_SUPPLY_ATTR(INPUT_VOLTAGE_LIMIT), + POWER_SUPPLY_ATTR(INPUT_POWER_LIMIT), + POWER_SUPPLY_ATTR(ENERGY_FULL_DESIGN), + POWER_SUPPLY_ATTR(ENERGY_EMPTY_DESIGN), + POWER_SUPPLY_ATTR(ENERGY_FULL), + POWER_SUPPLY_ATTR(ENERGY_EMPTY), + POWER_SUPPLY_ATTR(ENERGY_NOW), + POWER_SUPPLY_ATTR(ENERGY_AVG), + POWER_SUPPLY_ATTR(CAPACITY), + POWER_SUPPLY_ATTR(CAPACITY_ALERT_MIN), + POWER_SUPPLY_ATTR(CAPACITY_ALERT_MAX), + POWER_SUPPLY_ATTR(CAPACITY_ERROR_MARGIN), + POWER_SUPPLY_ENUM_ATTR(CAPACITY_LEVEL), + POWER_SUPPLY_ATTR(TEMP), + POWER_SUPPLY_ATTR(TEMP_MAX), + POWER_SUPPLY_ATTR(TEMP_MIN), + POWER_SUPPLY_ATTR(TEMP_ALERT_MIN), + POWER_SUPPLY_ATTR(TEMP_ALERT_MAX), + POWER_SUPPLY_ATTR(TEMP_AMBIENT), + POWER_SUPPLY_ATTR(TEMP_AMBIENT_ALERT_MIN), + POWER_SUPPLY_ATTR(TEMP_AMBIENT_ALERT_MAX), + POWER_SUPPLY_ATTR(TIME_TO_EMPTY_NOW), + POWER_SUPPLY_ATTR(TIME_TO_EMPTY_AVG), + POWER_SUPPLY_ATTR(TIME_TO_FULL_NOW), + POWER_SUPPLY_ATTR(TIME_TO_FULL_AVG), + POWER_SUPPLY_ENUM_ATTR(TYPE), + POWER_SUPPLY_ATTR(USB_TYPE), + POWER_SUPPLY_ENUM_ATTR(SCOPE), + POWER_SUPPLY_ATTR(PRECHARGE_CURRENT), + POWER_SUPPLY_ATTR(CHARGE_TERM_CURRENT), + POWER_SUPPLY_ATTR(CALIBRATE), + POWER_SUPPLY_ATTR(MANUFACTURE_YEAR), + POWER_SUPPLY_ATTR(MANUFACTURE_MONTH), + POWER_SUPPLY_ATTR(MANUFACTURE_DAY), + /* Properties of type `const char *' */ + POWER_SUPPLY_ATTR(MODEL_NAME), + POWER_SUPPLY_ATTR(MANUFACTURER), + POWER_SUPPLY_ATTR(SERIAL_NUMBER), +}; + +static struct attribute * +__power_supply_attrs[ARRAY_SIZE(power_supply_attrs) + 1]; + +static struct power_supply_attr *to_ps_attr(struct device_attribute *attr) +{ + return container_of(attr, struct power_supply_attr, dev_attr); +} + +static enum power_supply_property dev_attr_psp(struct device_attribute *attr) +{ + return to_ps_attr(attr) - power_supply_attrs; +} + +static ssize_t power_supply_show_usb_type(struct device *dev, + const struct power_supply_desc *desc, + union power_supply_propval *value, + char *buf) +{ + enum power_supply_usb_type usb_type; + ssize_t count = 0; + bool match = false; + int i; + + for (i = 0; i < desc->num_usb_types; ++i) { + usb_type = desc->usb_types[i]; + + if (value->intval == usb_type) { + count += sprintf(buf + count, "[%s] ", + POWER_SUPPLY_USB_TYPE_TEXT[usb_type]); + match = true; + } else { + count += sprintf(buf + count, "%s ", + POWER_SUPPLY_USB_TYPE_TEXT[usb_type]); + } + } + + if (!match) { + dev_warn(dev, "driver reporting unsupported connected type\n"); + return -EINVAL; + } + + if (count) + buf[count - 1] = '\n'; + + return count; +} + +static ssize_t power_supply_show_property(struct device *dev, + struct device_attribute *attr, + char *buf) { + ssize_t ret; + struct power_supply *psy = dev_get_drvdata(dev); + struct power_supply_attr *ps_attr = to_ps_attr(attr); + enum power_supply_property psp = dev_attr_psp(attr); + union power_supply_propval value; + + if (psp == POWER_SUPPLY_PROP_TYPE) { + value.intval = psy->desc->type; + } else { + ret = power_supply_get_property(psy, psp, &value); + + if (ret < 0) { + if (ret == -ENODATA) + dev_dbg_ratelimited(dev, + "driver has no data for `%s' property\n", + attr->attr.name); + else if (ret != -ENODEV && ret != -EAGAIN) + dev_err_ratelimited(dev, + "driver failed to report `%s' property: %zd\n", + attr->attr.name, ret); + return ret; + } + } + + if (ps_attr->text_values_len > 0 && + value.intval < ps_attr->text_values_len && value.intval >= 0) { + return sprintf(buf, "%s\n", ps_attr->text_values[value.intval]); + } + + switch (psp) { + case POWER_SUPPLY_PROP_USB_TYPE: + ret = power_supply_show_usb_type(dev, psy->desc, + &value, buf); + break; + case POWER_SUPPLY_PROP_MODEL_NAME ... POWER_SUPPLY_PROP_SERIAL_NUMBER: + ret = sprintf(buf, "%s\n", value.strval); + break; + default: + ret = sprintf(buf, "%d\n", value.intval); + } + + return ret; +} + +static ssize_t power_supply_store_property(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { + ssize_t ret; + struct power_supply *psy = dev_get_drvdata(dev); + struct power_supply_attr *ps_attr = to_ps_attr(attr); + enum power_supply_property psp = dev_attr_psp(attr); + union power_supply_propval value; + + ret = -EINVAL; + if (ps_attr->text_values_len > 0) { + ret = __sysfs_match_string(ps_attr->text_values, + ps_attr->text_values_len, buf); + } + + /* + * If no match was found, then check to see if it is an integer. + * Integer values are valid for enums in addition to the text value. + */ + if (ret < 0) { + long long_val; + + ret = kstrtol(buf, 10, &long_val); + if (ret < 0) + return ret; + + ret = long_val; + } + + value.intval = ret; + + ret = power_supply_set_property(psy, psp, &value); + if (ret < 0) + return ret; + + return count; +} + +static umode_t power_supply_attr_is_visible(struct kobject *kobj, + struct attribute *attr, + int attrno) +{ + struct device *dev = kobj_to_dev(kobj); + struct power_supply *psy = dev_get_drvdata(dev); + umode_t mode = S_IRUSR | S_IRGRP | S_IROTH; + int i; + + if (!power_supply_attrs[attrno].prop_name) + return 0; + + if (attrno == POWER_SUPPLY_PROP_TYPE) + return mode; + + for (i = 0; i < psy->desc->num_properties; i++) { + int property = psy->desc->properties[i]; + + if (property == attrno) { + if (psy->desc->property_is_writeable && + psy->desc->property_is_writeable(psy, property) > 0) + mode |= S_IWUSR; + + return mode; + } + } + + return 0; +} + +static struct attribute_group power_supply_attr_group = { + .attrs = __power_supply_attrs, + .is_visible = power_supply_attr_is_visible, +}; + +static const struct attribute_group *power_supply_attr_groups[] = { + &power_supply_attr_group, + NULL, +}; + +static void str_to_lower(char *str) +{ + while (*str) { + *str = tolower(*str); + str++; + } +} + +void power_supply_init_attrs(struct device_type *dev_type) +{ + int i; + + dev_type->groups = power_supply_attr_groups; + + for (i = 0; i < ARRAY_SIZE(power_supply_attrs); i++) { + struct device_attribute *attr; + + if (!power_supply_attrs[i].prop_name) { + pr_warn("%s: Property %d skipped because is is missing from power_supply_attrs\n", + __func__, i); + sprintf(power_supply_attrs[i].attr_name, "_err_%d", i); + } else { + str_to_lower(power_supply_attrs[i].attr_name); + } + + attr = &power_supply_attrs[i].dev_attr; + + attr->attr.name = power_supply_attrs[i].attr_name; + attr->show = power_supply_show_property; + attr->store = power_supply_store_property; + __power_supply_attrs[i] = &attr->attr; + } +} + +static int add_prop_uevent(struct device *dev, struct kobj_uevent_env *env, + enum power_supply_property prop, char *prop_buf) +{ + int ret = 0; + struct power_supply_attr *pwr_attr; + struct device_attribute *dev_attr; + char *line; + + pwr_attr = &power_supply_attrs[prop]; + dev_attr = &pwr_attr->dev_attr; + + ret = power_supply_show_property(dev, dev_attr, prop_buf); + if (ret == -ENODEV || ret == -ENODATA) { + /* + * When a battery is absent, we expect -ENODEV. Don't abort; + * send the uevent with at least the the PRESENT=0 property + */ + return 0; + } + + if (ret < 0) + return ret; + + line = strchr(prop_buf, '\n'); + if (line) + *line = 0; + + return add_uevent_var(env, "POWER_SUPPLY_%s=%s", + pwr_attr->prop_name, prop_buf); +} + +int power_supply_uevent(struct device *dev, struct kobj_uevent_env *env) +{ + struct power_supply *psy = dev_get_drvdata(dev); + int ret = 0, j; + char *prop_buf; + + if (!psy || !psy->desc) { + dev_dbg(dev, "No power supply yet\n"); + return ret; + } + + ret = add_uevent_var(env, "POWER_SUPPLY_NAME=%s", psy->desc->name); + if (ret) + return ret; + + prop_buf = (char *)get_zeroed_page(GFP_KERNEL); + if (!prop_buf) + return -ENOMEM; + + ret = add_prop_uevent(dev, env, POWER_SUPPLY_PROP_TYPE, prop_buf); + if (ret) + goto out; + + for (j = 0; j < psy->desc->num_properties; j++) { + ret = add_prop_uevent(dev, env, psy->desc->properties[j], + prop_buf); + if (ret) + goto out; + } + +out: + free_page((unsigned long)prop_buf); + + return ret; +} diff --git a/drivers/power/supply/qcom_smbb.c b/drivers/power/supply/qcom_smbb.c new file mode 100644 index 000000000..c890e1cec --- /dev/null +++ b/drivers/power/supply/qcom_smbb.c @@ -0,0 +1,1034 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2014, Sony Mobile Communications Inc. + * + * This driver is for the multi-block Switch-Mode Battery Charger and Boost + * (SMBB) hardware, found in Qualcomm PM8941 PMICs. The charger is an + * integrated, single-cell lithium-ion battery charger. + * + * Sub-components: + * - Charger core + * - Buck + * - DC charge-path + * - USB charge-path + * - Battery interface + * - Boost (not implemented) + * - Misc + * - HF-Buck + */ + +#include <linux/errno.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/extcon-provider.h> +#include <linux/regulator/driver.h> + +#define SMBB_CHG_VMAX 0x040 +#define SMBB_CHG_VSAFE 0x041 +#define SMBB_CHG_CFG 0x043 +#define SMBB_CHG_IMAX 0x044 +#define SMBB_CHG_ISAFE 0x045 +#define SMBB_CHG_VIN_MIN 0x047 +#define SMBB_CHG_CTRL 0x049 +#define CTRL_EN BIT(7) +#define SMBB_CHG_VBAT_WEAK 0x052 +#define SMBB_CHG_IBAT_TERM_CHG 0x05b +#define IBAT_TERM_CHG_IEOC BIT(7) +#define IBAT_TERM_CHG_IEOC_BMS BIT(7) +#define IBAT_TERM_CHG_IEOC_CHG 0 +#define SMBB_CHG_VBAT_DET 0x05d +#define SMBB_CHG_TCHG_MAX_EN 0x060 +#define TCHG_MAX_EN BIT(7) +#define SMBB_CHG_WDOG_TIME 0x062 +#define SMBB_CHG_WDOG_EN 0x065 +#define WDOG_EN BIT(7) + +#define SMBB_BUCK_REG_MODE 0x174 +#define BUCK_REG_MODE BIT(0) +#define BUCK_REG_MODE_VBAT BIT(0) +#define BUCK_REG_MODE_VSYS 0 + +#define SMBB_BAT_PRES_STATUS 0x208 +#define PRES_STATUS_BAT_PRES BIT(7) +#define SMBB_BAT_TEMP_STATUS 0x209 +#define TEMP_STATUS_OK BIT(7) +#define TEMP_STATUS_HOT BIT(6) +#define SMBB_BAT_BTC_CTRL 0x249 +#define BTC_CTRL_COMP_EN BIT(7) +#define BTC_CTRL_COLD_EXT BIT(1) +#define BTC_CTRL_HOT_EXT_N BIT(0) + +#define SMBB_USB_IMAX 0x344 +#define SMBB_USB_OTG_CTL 0x348 +#define OTG_CTL_EN BIT(0) +#define SMBB_USB_ENUM_TIMER_STOP 0x34e +#define ENUM_TIMER_STOP BIT(0) +#define SMBB_USB_SEC_ACCESS 0x3d0 +#define SEC_ACCESS_MAGIC 0xa5 +#define SMBB_USB_REV_BST 0x3ed +#define REV_BST_CHG_GONE BIT(7) + +#define SMBB_DC_IMAX 0x444 + +#define SMBB_MISC_REV2 0x601 +#define SMBB_MISC_BOOT_DONE 0x642 +#define BOOT_DONE BIT(7) + +#define STATUS_USBIN_VALID BIT(0) /* USB connection is valid */ +#define STATUS_DCIN_VALID BIT(1) /* DC connection is valid */ +#define STATUS_BAT_HOT BIT(2) /* Battery temp 1=Hot, 0=Cold */ +#define STATUS_BAT_OK BIT(3) /* Battery temp OK */ +#define STATUS_BAT_PRESENT BIT(4) /* Battery is present */ +#define STATUS_CHG_DONE BIT(5) /* Charge cycle is complete */ +#define STATUS_CHG_TRKL BIT(6) /* Trickle charging */ +#define STATUS_CHG_FAST BIT(7) /* Fast charging */ +#define STATUS_CHG_GONE BIT(8) /* No charger is connected */ + +enum smbb_attr { + ATTR_BAT_ISAFE, + ATTR_BAT_IMAX, + ATTR_USBIN_IMAX, + ATTR_DCIN_IMAX, + ATTR_BAT_VSAFE, + ATTR_BAT_VMAX, + ATTR_BAT_VMIN, + ATTR_CHG_VDET, + ATTR_VIN_MIN, + _ATTR_CNT, +}; + +struct smbb_charger { + unsigned int revision; + unsigned int addr; + struct device *dev; + struct extcon_dev *edev; + + bool dc_disabled; + bool jeita_ext_temp; + unsigned long status; + struct mutex statlock; + + unsigned int attr[_ATTR_CNT]; + + struct power_supply *usb_psy; + struct power_supply *dc_psy; + struct power_supply *bat_psy; + struct regmap *regmap; + + struct regulator_desc otg_rdesc; + struct regulator_dev *otg_reg; +}; + +static const unsigned int smbb_usb_extcon_cable[] = { + EXTCON_USB, + EXTCON_NONE, +}; + +static int smbb_vbat_weak_fn(unsigned int index) +{ + return 2100000 + index * 100000; +} + +static int smbb_vin_fn(unsigned int index) +{ + if (index > 42) + return 5600000 + (index - 43) * 200000; + return 3400000 + index * 50000; +} + +static int smbb_vmax_fn(unsigned int index) +{ + return 3240000 + index * 10000; +} + +static int smbb_vbat_det_fn(unsigned int index) +{ + return 3240000 + index * 20000; +} + +static int smbb_imax_fn(unsigned int index) +{ + if (index < 2) + return 100000 + index * 50000; + return index * 100000; +} + +static int smbb_bat_imax_fn(unsigned int index) +{ + return index * 50000; +} + +static unsigned int smbb_hw_lookup(unsigned int val, int (*fn)(unsigned int)) +{ + unsigned int widx; + unsigned int sel; + + for (widx = sel = 0; (*fn)(widx) <= val; ++widx) + sel = widx; + + return sel; +} + +static const struct smbb_charger_attr { + const char *name; + unsigned int reg; + unsigned int safe_reg; + unsigned int max; + unsigned int min; + unsigned int fail_ok; + int (*hw_fn)(unsigned int); +} smbb_charger_attrs[] = { + [ATTR_BAT_ISAFE] = { + .name = "qcom,fast-charge-safe-current", + .reg = SMBB_CHG_ISAFE, + .max = 3000000, + .min = 200000, + .hw_fn = smbb_bat_imax_fn, + .fail_ok = 1, + }, + [ATTR_BAT_IMAX] = { + .name = "qcom,fast-charge-current-limit", + .reg = SMBB_CHG_IMAX, + .safe_reg = SMBB_CHG_ISAFE, + .max = 3000000, + .min = 200000, + .hw_fn = smbb_bat_imax_fn, + }, + [ATTR_DCIN_IMAX] = { + .name = "qcom,dc-current-limit", + .reg = SMBB_DC_IMAX, + .max = 2500000, + .min = 100000, + .hw_fn = smbb_imax_fn, + }, + [ATTR_BAT_VSAFE] = { + .name = "qcom,fast-charge-safe-voltage", + .reg = SMBB_CHG_VSAFE, + .max = 5000000, + .min = 3240000, + .hw_fn = smbb_vmax_fn, + .fail_ok = 1, + }, + [ATTR_BAT_VMAX] = { + .name = "qcom,fast-charge-high-threshold-voltage", + .reg = SMBB_CHG_VMAX, + .safe_reg = SMBB_CHG_VSAFE, + .max = 5000000, + .min = 3240000, + .hw_fn = smbb_vmax_fn, + }, + [ATTR_BAT_VMIN] = { + .name = "qcom,fast-charge-low-threshold-voltage", + .reg = SMBB_CHG_VBAT_WEAK, + .max = 3600000, + .min = 2100000, + .hw_fn = smbb_vbat_weak_fn, + }, + [ATTR_CHG_VDET] = { + .name = "qcom,auto-recharge-threshold-voltage", + .reg = SMBB_CHG_VBAT_DET, + .max = 5000000, + .min = 3240000, + .hw_fn = smbb_vbat_det_fn, + }, + [ATTR_VIN_MIN] = { + .name = "qcom,minimum-input-voltage", + .reg = SMBB_CHG_VIN_MIN, + .max = 9600000, + .min = 4200000, + .hw_fn = smbb_vin_fn, + }, + [ATTR_USBIN_IMAX] = { + .name = "usb-charge-current-limit", + .reg = SMBB_USB_IMAX, + .max = 2500000, + .min = 100000, + .hw_fn = smbb_imax_fn, + }, +}; + +static int smbb_charger_attr_write(struct smbb_charger *chg, + enum smbb_attr which, unsigned int val) +{ + const struct smbb_charger_attr *prop; + unsigned int wval; + unsigned int out; + int rc; + + prop = &smbb_charger_attrs[which]; + + if (val > prop->max || val < prop->min) { + dev_err(chg->dev, "value out of range for %s [%u:%u]\n", + prop->name, prop->min, prop->max); + return -EINVAL; + } + + if (prop->safe_reg) { + rc = regmap_read(chg->regmap, + chg->addr + prop->safe_reg, &wval); + if (rc) { + dev_err(chg->dev, + "unable to read safe value for '%s'\n", + prop->name); + return rc; + } + + wval = prop->hw_fn(wval); + + if (val > wval) { + dev_warn(chg->dev, + "%s above safe value, clamping at %u\n", + prop->name, wval); + val = wval; + } + } + + wval = smbb_hw_lookup(val, prop->hw_fn); + + rc = regmap_write(chg->regmap, chg->addr + prop->reg, wval); + if (rc) { + dev_err(chg->dev, "unable to update %s", prop->name); + return rc; + } + out = prop->hw_fn(wval); + if (out != val) { + dev_warn(chg->dev, + "%s inaccurate, rounded to %u\n", + prop->name, out); + } + + dev_dbg(chg->dev, "%s <= %d\n", prop->name, out); + + chg->attr[which] = out; + + return 0; +} + +static int smbb_charger_attr_read(struct smbb_charger *chg, + enum smbb_attr which) +{ + const struct smbb_charger_attr *prop; + unsigned int val; + int rc; + + prop = &smbb_charger_attrs[which]; + + rc = regmap_read(chg->regmap, chg->addr + prop->reg, &val); + if (rc) { + dev_err(chg->dev, "failed to read %s\n", prop->name); + return rc; + } + val = prop->hw_fn(val); + dev_dbg(chg->dev, "%s => %d\n", prop->name, val); + + chg->attr[which] = val; + + return 0; +} + +static int smbb_charger_attr_parse(struct smbb_charger *chg, + enum smbb_attr which) +{ + const struct smbb_charger_attr *prop; + unsigned int val; + int rc; + + prop = &smbb_charger_attrs[which]; + + rc = of_property_read_u32(chg->dev->of_node, prop->name, &val); + if (rc == 0) { + rc = smbb_charger_attr_write(chg, which, val); + if (!rc || !prop->fail_ok) + return rc; + } + return smbb_charger_attr_read(chg, which); +} + +static void smbb_set_line_flag(struct smbb_charger *chg, int irq, int flag) +{ + bool state; + int ret; + + ret = irq_get_irqchip_state(irq, IRQCHIP_STATE_LINE_LEVEL, &state); + if (ret < 0) { + dev_err(chg->dev, "failed to read irq line\n"); + return; + } + + mutex_lock(&chg->statlock); + if (state) + chg->status |= flag; + else + chg->status &= ~flag; + mutex_unlock(&chg->statlock); + + dev_dbg(chg->dev, "status = %03lx\n", chg->status); +} + +static irqreturn_t smbb_usb_valid_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_USBIN_VALID); + extcon_set_state_sync(chg->edev, EXTCON_USB, + chg->status & STATUS_USBIN_VALID); + power_supply_changed(chg->usb_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_dc_valid_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_DCIN_VALID); + if (!chg->dc_disabled) + power_supply_changed(chg->dc_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_bat_temp_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + unsigned int val; + int rc; + + rc = regmap_read(chg->regmap, chg->addr + SMBB_BAT_TEMP_STATUS, &val); + if (rc) + return IRQ_HANDLED; + + mutex_lock(&chg->statlock); + if (val & TEMP_STATUS_OK) { + chg->status |= STATUS_BAT_OK; + } else { + chg->status &= ~STATUS_BAT_OK; + if (val & TEMP_STATUS_HOT) + chg->status |= STATUS_BAT_HOT; + } + mutex_unlock(&chg->statlock); + + power_supply_changed(chg->bat_psy); + return IRQ_HANDLED; +} + +static irqreturn_t smbb_bat_present_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_BAT_PRESENT); + power_supply_changed(chg->bat_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_chg_done_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_CHG_DONE); + power_supply_changed(chg->bat_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_chg_gone_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_CHG_GONE); + power_supply_changed(chg->bat_psy); + power_supply_changed(chg->usb_psy); + if (!chg->dc_disabled) + power_supply_changed(chg->dc_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_chg_fast_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_CHG_FAST); + power_supply_changed(chg->bat_psy); + + return IRQ_HANDLED; +} + +static irqreturn_t smbb_chg_trkl_handler(int irq, void *_data) +{ + struct smbb_charger *chg = _data; + + smbb_set_line_flag(chg, irq, STATUS_CHG_TRKL); + power_supply_changed(chg->bat_psy); + + return IRQ_HANDLED; +} + +static const struct smbb_irq { + const char *name; + irqreturn_t (*handler)(int, void *); +} smbb_charger_irqs[] = { + { "chg-done", smbb_chg_done_handler }, + { "chg-fast", smbb_chg_fast_handler }, + { "chg-trkl", smbb_chg_trkl_handler }, + { "bat-temp-ok", smbb_bat_temp_handler }, + { "bat-present", smbb_bat_present_handler }, + { "chg-gone", smbb_chg_gone_handler }, + { "usb-valid", smbb_usb_valid_handler }, + { "dc-valid", smbb_dc_valid_handler }, +}; + +static int smbb_usbin_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + int rc = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + mutex_lock(&chg->statlock); + val->intval = !(chg->status & STATUS_CHG_GONE) && + (chg->status & STATUS_USBIN_VALID); + mutex_unlock(&chg->statlock); + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + val->intval = chg->attr[ATTR_USBIN_IMAX]; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX: + val->intval = 2500000; + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_usbin_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + int rc; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + rc = smbb_charger_attr_write(chg, ATTR_USBIN_IMAX, + val->intval); + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_dcin_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + int rc = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + mutex_lock(&chg->statlock); + val->intval = !(chg->status & STATUS_CHG_GONE) && + (chg->status & STATUS_DCIN_VALID); + mutex_unlock(&chg->statlock); + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + val->intval = chg->attr[ATTR_DCIN_IMAX]; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX: + val->intval = 2500000; + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_dcin_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + int rc; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: + rc = smbb_charger_attr_write(chg, ATTR_DCIN_IMAX, + val->intval); + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_charger_writable_property(struct power_supply *psy, + enum power_supply_property psp) +{ + return psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT; +} + +static int smbb_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + unsigned long status; + int rc = 0; + + mutex_lock(&chg->statlock); + status = chg->status; + mutex_unlock(&chg->statlock); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (status & STATUS_CHG_GONE) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (!(status & (STATUS_DCIN_VALID | STATUS_USBIN_VALID))) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (status & STATUS_CHG_DONE) + val->intval = POWER_SUPPLY_STATUS_FULL; + else if (!(status & STATUS_BAT_OK)) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else if (status & (STATUS_CHG_FAST | STATUS_CHG_TRKL)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else /* everything is ok for charging, but we are not... */ + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + break; + case POWER_SUPPLY_PROP_HEALTH: + if (status & STATUS_BAT_OK) + val->intval = POWER_SUPPLY_HEALTH_GOOD; + else if (status & STATUS_BAT_HOT) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else + val->intval = POWER_SUPPLY_HEALTH_COLD; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + if (status & STATUS_CHG_FAST) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + else if (status & STATUS_CHG_TRKL) + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + else + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(status & STATUS_BAT_PRESENT); + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = chg->attr[ATTR_BAT_IMAX]; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + val->intval = chg->attr[ATTR_BAT_VMAX]; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + /* this charger is a single-cell lithium-ion battery charger + * only. If you hook up some other technology, there will be + * fireworks. + */ + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = 3000000; /* single-cell li-ion low end */ + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct smbb_charger *chg = power_supply_get_drvdata(psy); + int rc; + + switch (psp) { + case POWER_SUPPLY_PROP_CURRENT_MAX: + rc = smbb_charger_attr_write(chg, ATTR_BAT_IMAX, val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + rc = smbb_charger_attr_write(chg, ATTR_BAT_VMAX, val->intval); + break; + default: + rc = -EINVAL; + break; + } + + return rc; +} + +static int smbb_battery_writable_property(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CURRENT_MAX: + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + return 1; + default: + return 0; + } +} + +static enum power_supply_property smbb_charger_properties[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, + POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX, +}; + +static enum power_supply_property smbb_battery_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_TECHNOLOGY, +}; + +static const struct reg_off_mask_default { + unsigned int offset; + unsigned int mask; + unsigned int value; + unsigned int rev_mask; +} smbb_charger_setup[] = { + /* The bootloader is supposed to set this... make sure anyway. */ + { SMBB_MISC_BOOT_DONE, BOOT_DONE, BOOT_DONE }, + + /* Disable software timer */ + { SMBB_CHG_TCHG_MAX_EN, TCHG_MAX_EN, 0 }, + + /* Clear and disable watchdog */ + { SMBB_CHG_WDOG_TIME, 0xff, 160 }, + { SMBB_CHG_WDOG_EN, WDOG_EN, 0 }, + + /* Use charger based EoC detection */ + { SMBB_CHG_IBAT_TERM_CHG, IBAT_TERM_CHG_IEOC, IBAT_TERM_CHG_IEOC_CHG }, + + /* Disable GSM PA load adjustment. + * The PA signal is incorrectly connected on v2. + */ + { SMBB_CHG_CFG, 0xff, 0x00, BIT(3) }, + + /* Use VBAT (not VSYS) to compensate for IR drop during fast charging */ + { SMBB_BUCK_REG_MODE, BUCK_REG_MODE, BUCK_REG_MODE_VBAT }, + + /* Enable battery temperature comparators */ + { SMBB_BAT_BTC_CTRL, BTC_CTRL_COMP_EN, BTC_CTRL_COMP_EN }, + + /* Stop USB enumeration timer */ + { SMBB_USB_ENUM_TIMER_STOP, ENUM_TIMER_STOP, ENUM_TIMER_STOP }, + +#if 0 /* FIXME supposedly only to disable hardware ARB termination */ + { SMBB_USB_SEC_ACCESS, SEC_ACCESS_MAGIC }, + { SMBB_USB_REV_BST, 0xff, REV_BST_CHG_GONE }, +#endif + + /* Stop USB enumeration timer, again */ + { SMBB_USB_ENUM_TIMER_STOP, ENUM_TIMER_STOP, ENUM_TIMER_STOP }, + + /* Enable charging */ + { SMBB_CHG_CTRL, CTRL_EN, CTRL_EN }, +}; + +static char *smbb_bif[] = { "smbb-bif" }; + +static const struct power_supply_desc bat_psy_desc = { + .name = "smbb-bif", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = smbb_battery_properties, + .num_properties = ARRAY_SIZE(smbb_battery_properties), + .get_property = smbb_battery_get_property, + .set_property = smbb_battery_set_property, + .property_is_writeable = smbb_battery_writable_property, +}; + +static const struct power_supply_desc usb_psy_desc = { + .name = "smbb-usbin", + .type = POWER_SUPPLY_TYPE_USB, + .properties = smbb_charger_properties, + .num_properties = ARRAY_SIZE(smbb_charger_properties), + .get_property = smbb_usbin_get_property, + .set_property = smbb_usbin_set_property, + .property_is_writeable = smbb_charger_writable_property, +}; + +static const struct power_supply_desc dc_psy_desc = { + .name = "smbb-dcin", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = smbb_charger_properties, + .num_properties = ARRAY_SIZE(smbb_charger_properties), + .get_property = smbb_dcin_get_property, + .set_property = smbb_dcin_set_property, + .property_is_writeable = smbb_charger_writable_property, +}; + +static int smbb_chg_otg_enable(struct regulator_dev *rdev) +{ + struct smbb_charger *chg = rdev_get_drvdata(rdev); + int rc; + + rc = regmap_update_bits(chg->regmap, chg->addr + SMBB_USB_OTG_CTL, + OTG_CTL_EN, OTG_CTL_EN); + if (rc) + dev_err(chg->dev, "failed to update OTG_CTL\n"); + return rc; +} + +static int smbb_chg_otg_disable(struct regulator_dev *rdev) +{ + struct smbb_charger *chg = rdev_get_drvdata(rdev); + int rc; + + rc = regmap_update_bits(chg->regmap, chg->addr + SMBB_USB_OTG_CTL, + OTG_CTL_EN, 0); + if (rc) + dev_err(chg->dev, "failed to update OTG_CTL\n"); + return rc; +} + +static int smbb_chg_otg_is_enabled(struct regulator_dev *rdev) +{ + struct smbb_charger *chg = rdev_get_drvdata(rdev); + unsigned int value = 0; + int rc; + + rc = regmap_read(chg->regmap, chg->addr + SMBB_USB_OTG_CTL, &value); + if (rc) + dev_err(chg->dev, "failed to read OTG_CTL\n"); + + return !!(value & OTG_CTL_EN); +} + +static const struct regulator_ops smbb_chg_otg_ops = { + .enable = smbb_chg_otg_enable, + .disable = smbb_chg_otg_disable, + .is_enabled = smbb_chg_otg_is_enabled, +}; + +static int smbb_charger_probe(struct platform_device *pdev) +{ + struct power_supply_config bat_cfg = {}; + struct power_supply_config usb_cfg = {}; + struct power_supply_config dc_cfg = {}; + struct smbb_charger *chg; + struct regulator_config config = { }; + int rc, i; + + chg = devm_kzalloc(&pdev->dev, sizeof(*chg), GFP_KERNEL); + if (!chg) + return -ENOMEM; + + chg->dev = &pdev->dev; + mutex_init(&chg->statlock); + + chg->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!chg->regmap) { + dev_err(&pdev->dev, "failed to locate regmap\n"); + return -ENODEV; + } + + rc = of_property_read_u32(pdev->dev.of_node, "reg", &chg->addr); + if (rc) { + dev_err(&pdev->dev, "missing or invalid 'reg' property\n"); + return rc; + } + + rc = regmap_read(chg->regmap, chg->addr + SMBB_MISC_REV2, &chg->revision); + if (rc) { + dev_err(&pdev->dev, "unable to read revision\n"); + return rc; + } + + chg->revision += 1; + if (chg->revision != 2 && chg->revision != 3) { + dev_err(&pdev->dev, "v1 hardware not supported\n"); + return -ENODEV; + } + dev_info(&pdev->dev, "Initializing SMBB rev %u", chg->revision); + + chg->dc_disabled = of_property_read_bool(pdev->dev.of_node, "qcom,disable-dc"); + + for (i = 0; i < _ATTR_CNT; ++i) { + rc = smbb_charger_attr_parse(chg, i); + if (rc) { + dev_err(&pdev->dev, "failed to parse/apply settings\n"); + return rc; + } + } + + bat_cfg.drv_data = chg; + bat_cfg.of_node = pdev->dev.of_node; + chg->bat_psy = devm_power_supply_register(&pdev->dev, + &bat_psy_desc, + &bat_cfg); + if (IS_ERR(chg->bat_psy)) { + dev_err(&pdev->dev, "failed to register battery\n"); + return PTR_ERR(chg->bat_psy); + } + + usb_cfg.drv_data = chg; + usb_cfg.supplied_to = smbb_bif; + usb_cfg.num_supplicants = ARRAY_SIZE(smbb_bif); + chg->usb_psy = devm_power_supply_register(&pdev->dev, + &usb_psy_desc, + &usb_cfg); + if (IS_ERR(chg->usb_psy)) { + dev_err(&pdev->dev, "failed to register USB power supply\n"); + return PTR_ERR(chg->usb_psy); + } + + chg->edev = devm_extcon_dev_allocate(&pdev->dev, smbb_usb_extcon_cable); + if (IS_ERR(chg->edev)) { + dev_err(&pdev->dev, "failed to allocate extcon device\n"); + return -ENOMEM; + } + + rc = devm_extcon_dev_register(&pdev->dev, chg->edev); + if (rc < 0) { + dev_err(&pdev->dev, "failed to register extcon device\n"); + return rc; + } + + if (!chg->dc_disabled) { + dc_cfg.drv_data = chg; + dc_cfg.supplied_to = smbb_bif; + dc_cfg.num_supplicants = ARRAY_SIZE(smbb_bif); + chg->dc_psy = devm_power_supply_register(&pdev->dev, + &dc_psy_desc, + &dc_cfg); + if (IS_ERR(chg->dc_psy)) { + dev_err(&pdev->dev, "failed to register DC power supply\n"); + return PTR_ERR(chg->dc_psy); + } + } + + for (i = 0; i < ARRAY_SIZE(smbb_charger_irqs); ++i) { + int irq; + + irq = platform_get_irq_byname(pdev, smbb_charger_irqs[i].name); + if (irq < 0) { + dev_err(&pdev->dev, "failed to get irq '%s'\n", + smbb_charger_irqs[i].name); + return irq; + } + + smbb_charger_irqs[i].handler(irq, chg); + + rc = devm_request_threaded_irq(&pdev->dev, irq, NULL, + smbb_charger_irqs[i].handler, IRQF_ONESHOT, + smbb_charger_irqs[i].name, chg); + if (rc) { + dev_err(&pdev->dev, "failed to request irq '%s'\n", + smbb_charger_irqs[i].name); + return rc; + } + } + + /* + * otg regulator is used to control VBUS voltage direction + * when USB switches between host and gadget mode + */ + chg->otg_rdesc.id = -1; + chg->otg_rdesc.name = "otg-vbus"; + chg->otg_rdesc.ops = &smbb_chg_otg_ops; + chg->otg_rdesc.owner = THIS_MODULE; + chg->otg_rdesc.type = REGULATOR_VOLTAGE; + chg->otg_rdesc.supply_name = "usb-otg-in"; + chg->otg_rdesc.of_match = "otg-vbus"; + + config.dev = &pdev->dev; + config.driver_data = chg; + + chg->otg_reg = devm_regulator_register(&pdev->dev, &chg->otg_rdesc, + &config); + if (IS_ERR(chg->otg_reg)) + return PTR_ERR(chg->otg_reg); + + chg->jeita_ext_temp = of_property_read_bool(pdev->dev.of_node, + "qcom,jeita-extended-temp-range"); + + /* Set temperature range to [35%:70%] or [25%:80%] accordingly */ + rc = regmap_update_bits(chg->regmap, chg->addr + SMBB_BAT_BTC_CTRL, + BTC_CTRL_COLD_EXT | BTC_CTRL_HOT_EXT_N, + chg->jeita_ext_temp ? + BTC_CTRL_COLD_EXT : + BTC_CTRL_HOT_EXT_N); + if (rc) { + dev_err(&pdev->dev, + "unable to set %s temperature range\n", + chg->jeita_ext_temp ? "JEITA extended" : "normal"); + return rc; + } + + for (i = 0; i < ARRAY_SIZE(smbb_charger_setup); ++i) { + const struct reg_off_mask_default *r = &smbb_charger_setup[i]; + + if (r->rev_mask & BIT(chg->revision)) + continue; + + rc = regmap_update_bits(chg->regmap, chg->addr + r->offset, + r->mask, r->value); + if (rc) { + dev_err(&pdev->dev, + "unable to initializing charging, bailing\n"); + return rc; + } + } + + platform_set_drvdata(pdev, chg); + + return 0; +} + +static int smbb_charger_remove(struct platform_device *pdev) +{ + struct smbb_charger *chg; + + chg = platform_get_drvdata(pdev); + + regmap_update_bits(chg->regmap, chg->addr + SMBB_CHG_CTRL, CTRL_EN, 0); + + return 0; +} + +static const struct of_device_id smbb_charger_id_table[] = { + { .compatible = "qcom,pm8941-charger" }, + { } +}; +MODULE_DEVICE_TABLE(of, smbb_charger_id_table); + +static struct platform_driver smbb_charger_driver = { + .probe = smbb_charger_probe, + .remove = smbb_charger_remove, + .driver = { + .name = "qcom-smbb", + .of_match_table = smbb_charger_id_table, + }, +}; +module_platform_driver(smbb_charger_driver); + +MODULE_DESCRIPTION("Qualcomm Switch-Mode Battery Charger and Boost driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/rn5t618_power.c b/drivers/power/supply/rn5t618_power.c new file mode 100644 index 000000000..dee520f0f --- /dev/null +++ b/drivers/power/supply/rn5t618_power.c @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Power supply driver for the RICOH RN5T618 power management chip family + * + * Copyright (C) 2020 Andreas Kemnade + */ + +#include <linux/kernel.h> +#include <linux/device.h> +#include <linux/bitops.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/mfd/rn5t618.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#define CHG_STATE_ADP_INPUT 0x40 +#define CHG_STATE_USB_INPUT 0x80 +#define CHG_STATE_MASK 0x1f +#define CHG_STATE_CHG_OFF 0 +#define CHG_STATE_CHG_READY_VADP 1 +#define CHG_STATE_CHG_TRICKLE 2 +#define CHG_STATE_CHG_RAPID 3 +#define CHG_STATE_CHG_COMPLETE 4 +#define CHG_STATE_SUSPEND 5 +#define CHG_STATE_VCHG_OVER_VOL 6 +#define CHG_STATE_BAT_ERROR 7 +#define CHG_STATE_NO_BAT 8 +#define CHG_STATE_BAT_OVER_VOL 9 +#define CHG_STATE_BAT_TEMP_ERR 10 +#define CHG_STATE_DIE_ERR 11 +#define CHG_STATE_DIE_SHUTDOWN 12 +#define CHG_STATE_NO_BAT2 13 +#define CHG_STATE_CHG_READY_VUSB 14 + +#define FG_ENABLE 1 + +struct rn5t618_power_info { + struct rn5t618 *rn5t618; + struct platform_device *pdev; + struct power_supply *battery; + struct power_supply *usb; + struct power_supply *adp; + int irq; +}; + +static enum power_supply_property rn5t618_usb_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, +}; + +static enum power_supply_property rn5t618_adp_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, +}; + + +static enum power_supply_property rn5t618_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, +}; + +static int rn5t618_battery_read_doublereg(struct rn5t618_power_info *info, + u8 reg, u16 *result) +{ + int ret, i; + u8 data[2]; + u16 old, new; + + old = 0; + /* Prevent races when registers are changing. */ + for (i = 0; i < 3; i++) { + ret = regmap_bulk_read(info->rn5t618->regmap, + reg, data, sizeof(data)); + if (ret) + return ret; + + new = data[0] << 8; + new |= data[1]; + if (new == old) + break; + + old = new; + } + + *result = new; + + return 0; +} + +static int rn5t618_decode_status(unsigned int status) +{ + switch (status & CHG_STATE_MASK) { + case CHG_STATE_CHG_OFF: + case CHG_STATE_SUSPEND: + case CHG_STATE_VCHG_OVER_VOL: + case CHG_STATE_DIE_SHUTDOWN: + return POWER_SUPPLY_STATUS_DISCHARGING; + + case CHG_STATE_CHG_TRICKLE: + case CHG_STATE_CHG_RAPID: + return POWER_SUPPLY_STATUS_CHARGING; + + case CHG_STATE_CHG_COMPLETE: + return POWER_SUPPLY_STATUS_FULL; + + default: + return POWER_SUPPLY_STATUS_NOT_CHARGING; + } +} + +static int rn5t618_battery_status(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + unsigned int v; + int ret; + + ret = regmap_read(info->rn5t618->regmap, RN5T618_CHGSTATE, &v); + if (ret) + return ret; + + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + + if (v & 0xc0) { /* USB or ADP plugged */ + val->intval = rn5t618_decode_status(v); + } else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + + return ret; +} + +static int rn5t618_battery_present(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + unsigned int v; + int ret; + + ret = regmap_read(info->rn5t618->regmap, RN5T618_CHGSTATE, &v); + if (ret) + return ret; + + v &= CHG_STATE_MASK; + if ((v == CHG_STATE_NO_BAT) || (v == CHG_STATE_NO_BAT2)) + val->intval = 0; + else + val->intval = 1; + + return ret; +} + +static int rn5t618_battery_voltage_now(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_VOLTAGE_1, &res); + if (ret) + return ret; + + val->intval = res * 2 * 2500 / 4095 * 1000; + + return 0; +} + +static int rn5t618_battery_current_now(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_CC_AVEREG1, &res); + if (ret) + return ret; + + /* current is negative when discharging */ + val->intval = sign_extend32(res, 13) * 1000; + + return 0; +} + +static int rn5t618_battery_capacity(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + unsigned int v; + int ret; + + ret = regmap_read(info->rn5t618->regmap, RN5T618_SOC, &v); + if (ret) + return ret; + + val->intval = v; + + return 0; +} + +static int rn5t618_battery_temp(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_TEMP_1, &res); + if (ret) + return ret; + + val->intval = sign_extend32(res, 11) * 10 / 16; + + return 0; +} + +static int rn5t618_battery_tte(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_TT_EMPTY_H, &res); + if (ret) + return ret; + + if (res == 65535) + return -ENODATA; + + val->intval = res * 60; + + return 0; +} + +static int rn5t618_battery_ttf(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_TT_FULL_H, &res); + if (ret) + return ret; + + if (res == 65535) + return -ENODATA; + + val->intval = res * 60; + + return 0; +} + +static int rn5t618_battery_charge_full(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_FA_CAP_H, &res); + if (ret) + return ret; + + val->intval = res * 1000; + + return 0; +} + +static int rn5t618_battery_charge_now(struct rn5t618_power_info *info, + union power_supply_propval *val) +{ + u16 res; + int ret; + + ret = rn5t618_battery_read_doublereg(info, RN5T618_RE_CAP_H, &res); + if (ret) + return ret; + + val->intval = res * 1000; + + return 0; +} + +static int rn5t618_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct rn5t618_power_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = rn5t618_battery_status(info, val); + break; + case POWER_SUPPLY_PROP_PRESENT: + ret = rn5t618_battery_present(info, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = rn5t618_battery_voltage_now(info, val); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = rn5t618_battery_current_now(info, val); + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = rn5t618_battery_capacity(info, val); + break; + case POWER_SUPPLY_PROP_TEMP: + ret = rn5t618_battery_temp(info, val); + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + ret = rn5t618_battery_tte(info, val); + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + ret = rn5t618_battery_ttf(info, val); + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = rn5t618_battery_charge_full(info, val); + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = rn5t618_battery_charge_now(info, val); + break; + default: + return -EINVAL; + } + + return ret; +} + +static int rn5t618_adp_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct rn5t618_power_info *info = power_supply_get_drvdata(psy); + unsigned int chgstate; + bool online; + int ret; + + ret = regmap_read(info->rn5t618->regmap, RN5T618_CHGSTATE, &chgstate); + if (ret) + return ret; + + online = !!(chgstate & CHG_STATE_ADP_INPUT); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = online; + break; + case POWER_SUPPLY_PROP_STATUS: + if (!online) { + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + } + val->intval = rn5t618_decode_status(chgstate); + if (val->intval != POWER_SUPPLY_STATUS_CHARGING) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + + break; + default: + return -EINVAL; + } + + return 0; +} + +static int rn5t618_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct rn5t618_power_info *info = power_supply_get_drvdata(psy); + unsigned int chgstate; + bool online; + int ret; + + ret = regmap_read(info->rn5t618->regmap, RN5T618_CHGSTATE, &chgstate); + if (ret) + return ret; + + online = !!(chgstate & CHG_STATE_USB_INPUT); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = online; + break; + case POWER_SUPPLY_PROP_STATUS: + if (!online) { + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + } + val->intval = rn5t618_decode_status(chgstate); + if (val->intval != POWER_SUPPLY_STATUS_CHARGING) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct power_supply_desc rn5t618_battery_desc = { + .name = "rn5t618-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = rn5t618_battery_props, + .num_properties = ARRAY_SIZE(rn5t618_battery_props), + .get_property = rn5t618_battery_get_property, +}; + +static const struct power_supply_desc rn5t618_adp_desc = { + .name = "rn5t618-adp", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = rn5t618_adp_props, + .num_properties = ARRAY_SIZE(rn5t618_adp_props), + .get_property = rn5t618_adp_get_property, +}; + +static const struct power_supply_desc rn5t618_usb_desc = { + .name = "rn5t618-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = rn5t618_usb_props, + .num_properties = ARRAY_SIZE(rn5t618_usb_props), + .get_property = rn5t618_usb_get_property, +}; + +static irqreturn_t rn5t618_charger_irq(int irq, void *data) +{ + struct device *dev = data; + struct rn5t618_power_info *info = dev_get_drvdata(dev); + + unsigned int ctrl, stat1, stat2, err; + + regmap_read(info->rn5t618->regmap, RN5T618_CHGERR_IRR, &err); + regmap_read(info->rn5t618->regmap, RN5T618_CHGCTRL_IRR, &ctrl); + regmap_read(info->rn5t618->regmap, RN5T618_CHGSTAT_IRR1, &stat1); + regmap_read(info->rn5t618->regmap, RN5T618_CHGSTAT_IRR2, &stat2); + + regmap_write(info->rn5t618->regmap, RN5T618_CHGERR_IRR, 0); + regmap_write(info->rn5t618->regmap, RN5T618_CHGCTRL_IRR, 0); + regmap_write(info->rn5t618->regmap, RN5T618_CHGSTAT_IRR1, 0); + regmap_write(info->rn5t618->regmap, RN5T618_CHGSTAT_IRR2, 0); + + dev_dbg(dev, "chgerr: %x chgctrl: %x chgstat: %x chgstat2: %x\n", + err, ctrl, stat1, stat2); + + power_supply_changed(info->usb); + power_supply_changed(info->adp); + power_supply_changed(info->battery); + + return IRQ_HANDLED; +} + +static int rn5t618_power_probe(struct platform_device *pdev) +{ + int ret = 0; + unsigned int v; + struct power_supply_config psy_cfg = {}; + struct rn5t618_power_info *info; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->pdev = pdev; + info->rn5t618 = dev_get_drvdata(pdev->dev.parent); + info->irq = -1; + + platform_set_drvdata(pdev, info); + + ret = regmap_read(info->rn5t618->regmap, RN5T618_CONTROL, &v); + if (ret) + return ret; + + if (!(v & FG_ENABLE)) { + /* E.g. the vendor kernels of various Kobo and Tolino Ebook + * readers disable the fuel gauge on shutdown. If a kernel + * without fuel gauge support is booted after that, the fuel + * gauge will get decalibrated. + */ + dev_info(&pdev->dev, "Fuel gauge not enabled, enabling now\n"); + dev_info(&pdev->dev, "Expect imprecise results\n"); + regmap_update_bits(info->rn5t618->regmap, RN5T618_CONTROL, + FG_ENABLE, FG_ENABLE); + } + + psy_cfg.drv_data = info; + info->battery = devm_power_supply_register(&pdev->dev, + &rn5t618_battery_desc, + &psy_cfg); + if (IS_ERR(info->battery)) { + ret = PTR_ERR(info->battery); + dev_err(&pdev->dev, "failed to register battery: %d\n", ret); + return ret; + } + + info->adp = devm_power_supply_register(&pdev->dev, + &rn5t618_adp_desc, + &psy_cfg); + if (IS_ERR(info->adp)) { + ret = PTR_ERR(info->adp); + dev_err(&pdev->dev, "failed to register adp: %d\n", ret); + return ret; + } + + info->usb = devm_power_supply_register(&pdev->dev, + &rn5t618_usb_desc, + &psy_cfg); + if (IS_ERR(info->usb)) { + ret = PTR_ERR(info->usb); + dev_err(&pdev->dev, "failed to register usb: %d\n", ret); + return ret; + } + + if (info->rn5t618->irq_data) + info->irq = regmap_irq_get_virq(info->rn5t618->irq_data, + RN5T618_IRQ_CHG); + + if (info->irq < 0) + info->irq = -1; + else { + ret = devm_request_threaded_irq(&pdev->dev, info->irq, NULL, + rn5t618_charger_irq, + IRQF_ONESHOT, + "rn5t618_power", + &pdev->dev); + + if (ret < 0) { + dev_err(&pdev->dev, "request IRQ:%d fail\n", + info->irq); + info->irq = -1; + } + } + + return 0; +} + +static struct platform_driver rn5t618_power_driver = { + .driver = { + .name = "rn5t618-power", + }, + .probe = rn5t618_power_probe, +}; + +module_platform_driver(rn5t618_power_driver); +MODULE_ALIAS("platform:rn5t618-power"); +MODULE_DESCRIPTION("Power supply driver for RICOH RN5T618"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/rt5033_battery.c b/drivers/power/supply/rt5033_battery.c new file mode 100644 index 000000000..7a23c70f4 --- /dev/null +++ b/drivers/power/supply/rt5033_battery.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Fuel gauge driver for Richtek RT5033 + * + * Copyright (C) 2014 Samsung Electronics, Co., Ltd. + * Author: Beomho Seo <beomho.seo@samsung.com> + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/rt5033-private.h> +#include <linux/mfd/rt5033.h> + +static int rt5033_battery_get_capacity(struct i2c_client *client) +{ + struct rt5033_battery *battery = i2c_get_clientdata(client); + u32 msb; + + regmap_read(battery->regmap, RT5033_FUEL_REG_SOC_H, &msb); + + return msb; +} + +static int rt5033_battery_get_present(struct i2c_client *client) +{ + struct rt5033_battery *battery = i2c_get_clientdata(client); + u32 val; + + regmap_read(battery->regmap, RT5033_FUEL_REG_CONFIG_L, &val); + + return (val & RT5033_FUEL_BAT_PRESENT) ? true : false; +} + +static int rt5033_battery_get_watt_prop(struct i2c_client *client, + enum power_supply_property psp) +{ + struct rt5033_battery *battery = i2c_get_clientdata(client); + unsigned int regh, regl; + int ret; + u32 msb, lsb; + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + regh = RT5033_FUEL_REG_VBAT_H; + regl = RT5033_FUEL_REG_VBAT_L; + break; + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + regh = RT5033_FUEL_REG_AVG_VOLT_H; + regl = RT5033_FUEL_REG_AVG_VOLT_L; + break; + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + regh = RT5033_FUEL_REG_OCV_H; + regl = RT5033_FUEL_REG_OCV_L; + break; + default: + return -EINVAL; + } + + regmap_read(battery->regmap, regh, &msb); + regmap_read(battery->regmap, regl, &lsb); + + ret = ((msb << 4) + (lsb >> 4)) * 1250; + + return ret; +} + +static int rt5033_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct rt5033_battery *battery = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + val->intval = rt5033_battery_get_watt_prop(battery->client, + psp); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = rt5033_battery_get_present(battery->client); + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = rt5033_battery_get_capacity(battery->client); + break; + default: + return -EINVAL; + } + return 0; +} + +static enum power_supply_property rt5033_battery_props[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_VOLTAGE_OCV, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CAPACITY, +}; + +static const struct regmap_config rt5033_battery_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = RT5033_FUEL_REG_END, +}; + +static const struct power_supply_desc rt5033_battery_desc = { + .name = "rt5033-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = rt5033_battery_get_property, + .properties = rt5033_battery_props, + .num_properties = ARRAY_SIZE(rt5033_battery_props), +}; + +static int rt5033_battery_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct power_supply_config psy_cfg = {}; + struct rt5033_battery *battery; + u32 ret; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE)) + return -EIO; + + battery = devm_kzalloc(&client->dev, sizeof(*battery), GFP_KERNEL); + if (!battery) + return -ENOMEM; + + battery->client = client; + battery->regmap = devm_regmap_init_i2c(client, + &rt5033_battery_regmap_config); + if (IS_ERR(battery->regmap)) { + dev_err(&client->dev, "Failed to initialize regmap\n"); + return -EINVAL; + } + + i2c_set_clientdata(client, battery); + psy_cfg.drv_data = battery; + + battery->psy = power_supply_register(&client->dev, + &rt5033_battery_desc, &psy_cfg); + if (IS_ERR(battery->psy)) { + dev_err(&client->dev, "Failed to register power supply\n"); + ret = PTR_ERR(battery->psy); + return ret; + } + + return 0; +} + +static int rt5033_battery_remove(struct i2c_client *client) +{ + struct rt5033_battery *battery = i2c_get_clientdata(client); + + power_supply_unregister(battery->psy); + + return 0; +} + +static const struct i2c_device_id rt5033_battery_id[] = { + { "rt5033-battery", }, + { } +}; +MODULE_DEVICE_TABLE(i2c, rt5033_battery_id); + +static const struct of_device_id rt5033_battery_of_match[] = { + { .compatible = "richtek,rt5033-battery", }, + { } +}; +MODULE_DEVICE_TABLE(of, rt5033_battery_of_match); + +static struct i2c_driver rt5033_battery_driver = { + .driver = { + .name = "rt5033-battery", + .of_match_table = rt5033_battery_of_match, + }, + .probe = rt5033_battery_probe, + .remove = rt5033_battery_remove, + .id_table = rt5033_battery_id, +}; +module_i2c_driver(rt5033_battery_driver); + +MODULE_DESCRIPTION("Richtek RT5033 fuel gauge driver"); +MODULE_AUTHOR("Beomho Seo <beomho.seo@samsung.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/rt9455_charger.c b/drivers/power/supply/rt9455_charger.c new file mode 100644 index 000000000..594bb3b8a --- /dev/null +++ b/drivers/power/supply/rt9455_charger.c @@ -0,0 +1,1756 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for Richtek RT9455WSC battery charger. + * + * Copyright (C) 2015 Intel Corporation + */ + +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/delay.h> +#include <linux/of_irq.h> +#include <linux/of_device.h> +#include <linux/pm_runtime.h> +#include <linux/power_supply.h> +#include <linux/i2c.h> +#include <linux/acpi.h> +#include <linux/usb/phy.h> +#include <linux/regmap.h> + +#define RT9455_MANUFACTURER "Richtek" +#define RT9455_MODEL_NAME "RT9455" +#define RT9455_DRIVER_NAME "rt9455-charger" + +#define RT9455_IRQ_NAME "interrupt" + +#define RT9455_PWR_RDY_DELAY 1 /* 1 second */ +#define RT9455_MAX_CHARGING_TIME 21600 /* 6 hrs */ +#define RT9455_BATT_PRESENCE_DELAY 60 /* 60 seconds */ + +#define RT9455_CHARGE_MODE 0x00 +#define RT9455_BOOST_MODE 0x01 + +#define RT9455_FAULT 0x03 + +#define RT9455_IAICR_100MA 0x00 +#define RT9455_IAICR_500MA 0x01 +#define RT9455_IAICR_NO_LIMIT 0x03 + +#define RT9455_CHARGE_DISABLE 0x00 +#define RT9455_CHARGE_ENABLE 0x01 + +#define RT9455_PWR_FAULT 0x00 +#define RT9455_PWR_GOOD 0x01 + +#define RT9455_REG_CTRL1 0x00 /* CTRL1 reg address */ +#define RT9455_REG_CTRL2 0x01 /* CTRL2 reg address */ +#define RT9455_REG_CTRL3 0x02 /* CTRL3 reg address */ +#define RT9455_REG_DEV_ID 0x03 /* DEV_ID reg address */ +#define RT9455_REG_CTRL4 0x04 /* CTRL4 reg address */ +#define RT9455_REG_CTRL5 0x05 /* CTRL5 reg address */ +#define RT9455_REG_CTRL6 0x06 /* CTRL6 reg address */ +#define RT9455_REG_CTRL7 0x07 /* CTRL7 reg address */ +#define RT9455_REG_IRQ1 0x08 /* IRQ1 reg address */ +#define RT9455_REG_IRQ2 0x09 /* IRQ2 reg address */ +#define RT9455_REG_IRQ3 0x0A /* IRQ3 reg address */ +#define RT9455_REG_MASK1 0x0B /* MASK1 reg address */ +#define RT9455_REG_MASK2 0x0C /* MASK2 reg address */ +#define RT9455_REG_MASK3 0x0D /* MASK3 reg address */ + +enum rt9455_fields { + F_STAT, F_BOOST, F_PWR_RDY, F_OTG_PIN_POLARITY, /* CTRL1 reg fields */ + + F_IAICR, F_TE_SHDN_EN, F_HIGHER_OCP, F_TE, F_IAICR_INT, F_HIZ, + F_OPA_MODE, /* CTRL2 reg fields */ + + F_VOREG, F_OTG_PL, F_OTG_EN, /* CTRL3 reg fields */ + + F_VENDOR_ID, F_CHIP_REV, /* DEV_ID reg fields */ + + F_RST, /* CTRL4 reg fields */ + + F_TMR_EN, F_MIVR, F_IPREC, F_IEOC_PERCENTAGE, /* CTRL5 reg fields*/ + + F_IAICR_SEL, F_ICHRG, F_VPREC, /* CTRL6 reg fields */ + + F_BATD_EN, F_CHG_EN, F_VMREG, /* CTRL7 reg fields */ + + F_TSDI, F_VINOVPI, F_BATAB, /* IRQ1 reg fields */ + + F_CHRVPI, F_CHBATOVI, F_CHTERMI, F_CHRCHGI, F_CH32MI, F_CHTREGI, + F_CHMIVRI, /* IRQ2 reg fields */ + + F_BSTBUSOVI, F_BSTOLI, F_BSTLOWVI, F_BST32SI, /* IRQ3 reg fields */ + + F_TSDM, F_VINOVPIM, F_BATABM, /* MASK1 reg fields */ + + F_CHRVPIM, F_CHBATOVIM, F_CHTERMIM, F_CHRCHGIM, F_CH32MIM, F_CHTREGIM, + F_CHMIVRIM, /* MASK2 reg fields */ + + F_BSTVINOVIM, F_BSTOLIM, F_BSTLOWVIM, F_BST32SIM, /* MASK3 reg fields */ + + F_MAX_FIELDS +}; + +static const struct reg_field rt9455_reg_fields[] = { + [F_STAT] = REG_FIELD(RT9455_REG_CTRL1, 4, 5), + [F_BOOST] = REG_FIELD(RT9455_REG_CTRL1, 3, 3), + [F_PWR_RDY] = REG_FIELD(RT9455_REG_CTRL1, 2, 2), + [F_OTG_PIN_POLARITY] = REG_FIELD(RT9455_REG_CTRL1, 1, 1), + + [F_IAICR] = REG_FIELD(RT9455_REG_CTRL2, 6, 7), + [F_TE_SHDN_EN] = REG_FIELD(RT9455_REG_CTRL2, 5, 5), + [F_HIGHER_OCP] = REG_FIELD(RT9455_REG_CTRL2, 4, 4), + [F_TE] = REG_FIELD(RT9455_REG_CTRL2, 3, 3), + [F_IAICR_INT] = REG_FIELD(RT9455_REG_CTRL2, 2, 2), + [F_HIZ] = REG_FIELD(RT9455_REG_CTRL2, 1, 1), + [F_OPA_MODE] = REG_FIELD(RT9455_REG_CTRL2, 0, 0), + + [F_VOREG] = REG_FIELD(RT9455_REG_CTRL3, 2, 7), + [F_OTG_PL] = REG_FIELD(RT9455_REG_CTRL3, 1, 1), + [F_OTG_EN] = REG_FIELD(RT9455_REG_CTRL3, 0, 0), + + [F_VENDOR_ID] = REG_FIELD(RT9455_REG_DEV_ID, 4, 7), + [F_CHIP_REV] = REG_FIELD(RT9455_REG_DEV_ID, 0, 3), + + [F_RST] = REG_FIELD(RT9455_REG_CTRL4, 7, 7), + + [F_TMR_EN] = REG_FIELD(RT9455_REG_CTRL5, 7, 7), + [F_MIVR] = REG_FIELD(RT9455_REG_CTRL5, 4, 5), + [F_IPREC] = REG_FIELD(RT9455_REG_CTRL5, 2, 3), + [F_IEOC_PERCENTAGE] = REG_FIELD(RT9455_REG_CTRL5, 0, 1), + + [F_IAICR_SEL] = REG_FIELD(RT9455_REG_CTRL6, 7, 7), + [F_ICHRG] = REG_FIELD(RT9455_REG_CTRL6, 4, 6), + [F_VPREC] = REG_FIELD(RT9455_REG_CTRL6, 0, 2), + + [F_BATD_EN] = REG_FIELD(RT9455_REG_CTRL7, 6, 6), + [F_CHG_EN] = REG_FIELD(RT9455_REG_CTRL7, 4, 4), + [F_VMREG] = REG_FIELD(RT9455_REG_CTRL7, 0, 3), + + [F_TSDI] = REG_FIELD(RT9455_REG_IRQ1, 7, 7), + [F_VINOVPI] = REG_FIELD(RT9455_REG_IRQ1, 6, 6), + [F_BATAB] = REG_FIELD(RT9455_REG_IRQ1, 0, 0), + + [F_CHRVPI] = REG_FIELD(RT9455_REG_IRQ2, 7, 7), + [F_CHBATOVI] = REG_FIELD(RT9455_REG_IRQ2, 5, 5), + [F_CHTERMI] = REG_FIELD(RT9455_REG_IRQ2, 4, 4), + [F_CHRCHGI] = REG_FIELD(RT9455_REG_IRQ2, 3, 3), + [F_CH32MI] = REG_FIELD(RT9455_REG_IRQ2, 2, 2), + [F_CHTREGI] = REG_FIELD(RT9455_REG_IRQ2, 1, 1), + [F_CHMIVRI] = REG_FIELD(RT9455_REG_IRQ2, 0, 0), + + [F_BSTBUSOVI] = REG_FIELD(RT9455_REG_IRQ3, 7, 7), + [F_BSTOLI] = REG_FIELD(RT9455_REG_IRQ3, 6, 6), + [F_BSTLOWVI] = REG_FIELD(RT9455_REG_IRQ3, 5, 5), + [F_BST32SI] = REG_FIELD(RT9455_REG_IRQ3, 3, 3), + + [F_TSDM] = REG_FIELD(RT9455_REG_MASK1, 7, 7), + [F_VINOVPIM] = REG_FIELD(RT9455_REG_MASK1, 6, 6), + [F_BATABM] = REG_FIELD(RT9455_REG_MASK1, 0, 0), + + [F_CHRVPIM] = REG_FIELD(RT9455_REG_MASK2, 7, 7), + [F_CHBATOVIM] = REG_FIELD(RT9455_REG_MASK2, 5, 5), + [F_CHTERMIM] = REG_FIELD(RT9455_REG_MASK2, 4, 4), + [F_CHRCHGIM] = REG_FIELD(RT9455_REG_MASK2, 3, 3), + [F_CH32MIM] = REG_FIELD(RT9455_REG_MASK2, 2, 2), + [F_CHTREGIM] = REG_FIELD(RT9455_REG_MASK2, 1, 1), + [F_CHMIVRIM] = REG_FIELD(RT9455_REG_MASK2, 0, 0), + + [F_BSTVINOVIM] = REG_FIELD(RT9455_REG_MASK3, 7, 7), + [F_BSTOLIM] = REG_FIELD(RT9455_REG_MASK3, 6, 6), + [F_BSTLOWVIM] = REG_FIELD(RT9455_REG_MASK3, 5, 5), + [F_BST32SIM] = REG_FIELD(RT9455_REG_MASK3, 3, 3), +}; + +#define GET_MASK(fid) (BIT(rt9455_reg_fields[fid].msb + 1) - \ + BIT(rt9455_reg_fields[fid].lsb)) + +/* + * Each array initialised below shows the possible real-world values for a + * group of bits belonging to RT9455 registers. The arrays are sorted in + * ascending order. The index of each real-world value represents the value + * that is encoded in the group of bits belonging to RT9455 registers. + */ +/* REG06[6:4] (ICHRG) in uAh */ +static const int rt9455_ichrg_values[] = { + 500000, 650000, 800000, 950000, 1100000, 1250000, 1400000, 1550000 +}; + +/* + * When the charger is in charge mode, REG02[7:2] represent battery regulation + * voltage. + */ +/* REG02[7:2] (VOREG) in uV */ +static const int rt9455_voreg_values[] = { + 3500000, 3520000, 3540000, 3560000, 3580000, 3600000, 3620000, 3640000, + 3660000, 3680000, 3700000, 3720000, 3740000, 3760000, 3780000, 3800000, + 3820000, 3840000, 3860000, 3880000, 3900000, 3920000, 3940000, 3960000, + 3980000, 4000000, 4020000, 4040000, 4060000, 4080000, 4100000, 4120000, + 4140000, 4160000, 4180000, 4200000, 4220000, 4240000, 4260000, 4280000, + 4300000, 4330000, 4350000, 4370000, 4390000, 4410000, 4430000, 4450000, + 4450000, 4450000, 4450000, 4450000, 4450000, 4450000, 4450000, 4450000, + 4450000, 4450000, 4450000, 4450000, 4450000, 4450000, 4450000, 4450000 +}; + +/* + * When the charger is in boost mode, REG02[7:2] represent boost output + * voltage. + */ +/* REG02[7:2] (Boost output voltage) in uV */ +static const int rt9455_boost_voltage_values[] = { + 4425000, 4450000, 4475000, 4500000, 4525000, 4550000, 4575000, 4600000, + 4625000, 4650000, 4675000, 4700000, 4725000, 4750000, 4775000, 4800000, + 4825000, 4850000, 4875000, 4900000, 4925000, 4950000, 4975000, 5000000, + 5025000, 5050000, 5075000, 5100000, 5125000, 5150000, 5175000, 5200000, + 5225000, 5250000, 5275000, 5300000, 5325000, 5350000, 5375000, 5400000, + 5425000, 5450000, 5475000, 5500000, 5525000, 5550000, 5575000, 5600000, + 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, + 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, 5600000, +}; + +/* REG07[3:0] (VMREG) in uV */ +static const int rt9455_vmreg_values[] = { + 4200000, 4220000, 4240000, 4260000, 4280000, 4300000, 4320000, 4340000, + 4360000, 4380000, 4400000, 4430000, 4450000, 4450000, 4450000, 4450000 +}; + +/* REG05[5:4] (IEOC_PERCENTAGE) */ +static const int rt9455_ieoc_percentage_values[] = { + 10, 30, 20, 30 +}; + +/* REG05[1:0] (MIVR) in uV */ +static const int rt9455_mivr_values[] = { + 4000000, 4250000, 4500000, 5000000 +}; + +/* REG05[1:0] (IAICR) in uA */ +static const int rt9455_iaicr_values[] = { + 100000, 500000, 1000000, 2000000 +}; + +struct rt9455_info { + struct i2c_client *client; + struct regmap *regmap; + struct regmap_field *regmap_fields[F_MAX_FIELDS]; + struct power_supply *charger; +#if IS_ENABLED(CONFIG_USB_PHY) + struct usb_phy *usb_phy; + struct notifier_block nb; +#endif + struct delayed_work pwr_rdy_work; + struct delayed_work max_charging_time_work; + struct delayed_work batt_presence_work; + u32 voreg; + u32 boost_voltage; +}; + +/* + * Iterate through each element of the 'tbl' array until an element whose value + * is greater than v is found. Return the index of the respective element, + * or the index of the last element in the array, if no such element is found. + */ +static unsigned int rt9455_find_idx(const int tbl[], int tbl_size, int v) +{ + int i; + + /* + * No need to iterate until the last index in the table because + * if no element greater than v is found in the table, + * or if only the last element is greater than v, + * function returns the index of the last element. + */ + for (i = 0; i < tbl_size - 1; i++) + if (v <= tbl[i]) + return i; + + return (tbl_size - 1); +} + +static int rt9455_get_field_val(struct rt9455_info *info, + enum rt9455_fields field, + const int tbl[], int tbl_size, int *val) +{ + unsigned int v; + int ret; + + ret = regmap_field_read(info->regmap_fields[field], &v); + if (ret) + return ret; + + v = (v >= tbl_size) ? (tbl_size - 1) : v; + *val = tbl[v]; + + return 0; +} + +static int rt9455_set_field_val(struct rt9455_info *info, + enum rt9455_fields field, + const int tbl[], int tbl_size, int val) +{ + unsigned int idx = rt9455_find_idx(tbl, tbl_size, val); + + return regmap_field_write(info->regmap_fields[field], idx); +} + +static int rt9455_register_reset(struct rt9455_info *info) +{ + struct device *dev = &info->client->dev; + unsigned int v; + int ret, limit = 100; + + ret = regmap_field_write(info->regmap_fields[F_RST], 0x01); + if (ret) { + dev_err(dev, "Failed to set RST bit\n"); + return ret; + } + + /* + * To make sure that reset operation has finished, loop until RST bit + * is set to 0. + */ + do { + ret = regmap_field_read(info->regmap_fields[F_RST], &v); + if (ret) { + dev_err(dev, "Failed to read RST bit\n"); + return ret; + } + + if (!v) + break; + + usleep_range(10, 100); + } while (--limit); + + if (!limit) + return -EIO; + + return 0; +} + +/* Charger power supply property routines */ +static enum power_supply_property rt9455_charger_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + 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, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, +}; + +static char *rt9455_charger_supplied_to[] = { + "main-battery", +}; + +static int rt9455_charger_get_status(struct rt9455_info *info, + union power_supply_propval *val) +{ + unsigned int v, pwr_rdy; + int ret; + + ret = regmap_field_read(info->regmap_fields[F_PWR_RDY], + &pwr_rdy); + if (ret) { + dev_err(&info->client->dev, "Failed to read PWR_RDY bit\n"); + return ret; + } + + /* + * If PWR_RDY bit is unset, the battery is discharging. Otherwise, + * STAT bits value must be checked. + */ + if (!pwr_rdy) { + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + ret = regmap_field_read(info->regmap_fields[F_STAT], &v); + if (ret) { + dev_err(&info->client->dev, "Failed to read STAT bits\n"); + return ret; + } + + switch (v) { + case 0: + /* + * If PWR_RDY bit is set, but STAT bits value is 0, the charger + * may be in one of the following cases: + * 1. CHG_EN bit is 0. + * 2. CHG_EN bit is 1 but the battery is not connected. + * In any of these cases, POWER_SUPPLY_STATUS_NOT_CHARGING is + * returned. + */ + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + return 0; + case 1: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + return 0; + case 2: + val->intval = POWER_SUPPLY_STATUS_FULL; + return 0; + default: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + return 0; + } +} + +static int rt9455_charger_get_health(struct rt9455_info *info, + union power_supply_propval *val) +{ + struct device *dev = &info->client->dev; + unsigned int v; + int ret; + + val->intval = POWER_SUPPLY_HEALTH_GOOD; + + ret = regmap_read(info->regmap, RT9455_REG_IRQ1, &v); + if (ret) { + dev_err(dev, "Failed to read IRQ1 register\n"); + return ret; + } + + if (v & GET_MASK(F_TSDI)) { + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + return 0; + } + if (v & GET_MASK(F_VINOVPI)) { + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + return 0; + } + if (v & GET_MASK(F_BATAB)) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + + ret = regmap_read(info->regmap, RT9455_REG_IRQ2, &v); + if (ret) { + dev_err(dev, "Failed to read IRQ2 register\n"); + return ret; + } + + if (v & GET_MASK(F_CHBATOVI)) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + if (v & GET_MASK(F_CH32MI)) { + val->intval = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + return 0; + } + + ret = regmap_read(info->regmap, RT9455_REG_IRQ3, &v); + if (ret) { + dev_err(dev, "Failed to read IRQ3 register\n"); + return ret; + } + + if (v & GET_MASK(F_BSTBUSOVI)) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + if (v & GET_MASK(F_BSTOLI)) { + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + return 0; + } + if (v & GET_MASK(F_BSTLOWVI)) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + if (v & GET_MASK(F_BST32SI)) { + val->intval = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; + return 0; + } + + ret = regmap_field_read(info->regmap_fields[F_STAT], &v); + if (ret) { + dev_err(dev, "Failed to read STAT bits\n"); + return ret; + } + + if (v == RT9455_FAULT) { + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + return 0; + } + + return 0; +} + +static int rt9455_charger_get_battery_presence(struct rt9455_info *info, + union power_supply_propval *val) +{ + unsigned int v; + int ret; + + ret = regmap_field_read(info->regmap_fields[F_BATAB], &v); + if (ret) { + dev_err(&info->client->dev, "Failed to read BATAB bit\n"); + return ret; + } + + /* + * Since BATAB is 1 when battery is NOT present and 0 otherwise, + * !BATAB is returned. + */ + val->intval = !v; + + return 0; +} + +static int rt9455_charger_get_online(struct rt9455_info *info, + union power_supply_propval *val) +{ + unsigned int v; + int ret; + + ret = regmap_field_read(info->regmap_fields[F_PWR_RDY], &v); + if (ret) { + dev_err(&info->client->dev, "Failed to read PWR_RDY bit\n"); + return ret; + } + + val->intval = (int)v; + + return 0; +} + +static int rt9455_charger_get_current(struct rt9455_info *info, + union power_supply_propval *val) +{ + int curr; + int ret; + + ret = rt9455_get_field_val(info, F_ICHRG, + rt9455_ichrg_values, + ARRAY_SIZE(rt9455_ichrg_values), + &curr); + if (ret) { + dev_err(&info->client->dev, "Failed to read ICHRG value\n"); + return ret; + } + + val->intval = curr; + + return 0; +} + +static int rt9455_charger_get_current_max(struct rt9455_info *info, + union power_supply_propval *val) +{ + int idx = ARRAY_SIZE(rt9455_ichrg_values) - 1; + + val->intval = rt9455_ichrg_values[idx]; + + return 0; +} + +static int rt9455_charger_get_voltage(struct rt9455_info *info, + union power_supply_propval *val) +{ + int voltage; + int ret; + + ret = rt9455_get_field_val(info, F_VOREG, + rt9455_voreg_values, + ARRAY_SIZE(rt9455_voreg_values), + &voltage); + if (ret) { + dev_err(&info->client->dev, "Failed to read VOREG value\n"); + return ret; + } + + val->intval = voltage; + + return 0; +} + +static int rt9455_charger_get_voltage_max(struct rt9455_info *info, + union power_supply_propval *val) +{ + int idx = ARRAY_SIZE(rt9455_vmreg_values) - 1; + + val->intval = rt9455_vmreg_values[idx]; + + return 0; +} + +static int rt9455_charger_get_term_current(struct rt9455_info *info, + union power_supply_propval *val) +{ + struct device *dev = &info->client->dev; + int ichrg, ieoc_percentage, ret; + + ret = rt9455_get_field_val(info, F_ICHRG, + rt9455_ichrg_values, + ARRAY_SIZE(rt9455_ichrg_values), + &ichrg); + if (ret) { + dev_err(dev, "Failed to read ICHRG value\n"); + return ret; + } + + ret = rt9455_get_field_val(info, F_IEOC_PERCENTAGE, + rt9455_ieoc_percentage_values, + ARRAY_SIZE(rt9455_ieoc_percentage_values), + &ieoc_percentage); + if (ret) { + dev_err(dev, "Failed to read IEOC value\n"); + return ret; + } + + val->intval = ichrg * ieoc_percentage / 100; + + return 0; +} + +static int rt9455_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct rt9455_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + return rt9455_charger_get_status(info, val); + case POWER_SUPPLY_PROP_HEALTH: + return rt9455_charger_get_health(info, val); + case POWER_SUPPLY_PROP_PRESENT: + return rt9455_charger_get_battery_presence(info, val); + case POWER_SUPPLY_PROP_ONLINE: + return rt9455_charger_get_online(info, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + return rt9455_charger_get_current(info, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + return rt9455_charger_get_current_max(info, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + return rt9455_charger_get_voltage(info, val); + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + return rt9455_charger_get_voltage_max(info, val); + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + return 0; + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + return rt9455_charger_get_term_current(info, val); + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = RT9455_MODEL_NAME; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = RT9455_MANUFACTURER; + return 0; + default: + return -ENODATA; + } +} + +static int rt9455_hw_init(struct rt9455_info *info, u32 ichrg, + u32 ieoc_percentage, + u32 mivr, u32 iaicr) +{ + struct device *dev = &info->client->dev; + int idx, ret; + + ret = rt9455_register_reset(info); + if (ret) { + dev_err(dev, "Power On Reset failed\n"); + return ret; + } + + /* Set TE bit in order to enable end of charge detection */ + ret = regmap_field_write(info->regmap_fields[F_TE], 1); + if (ret) { + dev_err(dev, "Failed to set TE bit\n"); + return ret; + } + + /* Set TE_SHDN_EN bit in order to enable end of charge detection */ + ret = regmap_field_write(info->regmap_fields[F_TE_SHDN_EN], 1); + if (ret) { + dev_err(dev, "Failed to set TE_SHDN_EN bit\n"); + return ret; + } + + /* + * Set BATD_EN bit in order to enable battery detection + * when charging is done + */ + ret = regmap_field_write(info->regmap_fields[F_BATD_EN], 1); + if (ret) { + dev_err(dev, "Failed to set BATD_EN bit\n"); + return ret; + } + + /* + * Disable Safety Timer. In charge mode, this timer terminates charging + * if no read or write via I2C is done within 32 minutes. This timer + * avoids overcharging the baterry when the OS is not loaded and the + * charger is connected to a power source. + * In boost mode, this timer triggers BST32SI interrupt if no read or + * write via I2C is done within 32 seconds. + * When the OS is loaded and the charger driver is inserted, it is used + * delayed_work, named max_charging_time_work, to avoid overcharging + * the battery. + */ + ret = regmap_field_write(info->regmap_fields[F_TMR_EN], 0x00); + if (ret) { + dev_err(dev, "Failed to disable Safety Timer\n"); + return ret; + } + + /* Set ICHRG to value retrieved from device-specific data */ + ret = rt9455_set_field_val(info, F_ICHRG, + rt9455_ichrg_values, + ARRAY_SIZE(rt9455_ichrg_values), ichrg); + if (ret) { + dev_err(dev, "Failed to set ICHRG value\n"); + return ret; + } + + /* Set IEOC Percentage to value retrieved from device-specific data */ + ret = rt9455_set_field_val(info, F_IEOC_PERCENTAGE, + rt9455_ieoc_percentage_values, + ARRAY_SIZE(rt9455_ieoc_percentage_values), + ieoc_percentage); + if (ret) { + dev_err(dev, "Failed to set IEOC Percentage value\n"); + return ret; + } + + /* Set VOREG to value retrieved from device-specific data */ + ret = rt9455_set_field_val(info, F_VOREG, + rt9455_voreg_values, + ARRAY_SIZE(rt9455_voreg_values), + info->voreg); + if (ret) { + dev_err(dev, "Failed to set VOREG value\n"); + return ret; + } + + /* Set VMREG value to maximum (4.45V). */ + idx = ARRAY_SIZE(rt9455_vmreg_values) - 1; + ret = rt9455_set_field_val(info, F_VMREG, + rt9455_vmreg_values, + ARRAY_SIZE(rt9455_vmreg_values), + rt9455_vmreg_values[idx]); + if (ret) { + dev_err(dev, "Failed to set VMREG value\n"); + return ret; + } + + /* + * Set MIVR to value retrieved from device-specific data. + * If no value is specified, default value for MIVR is 4.5V. + */ + if (mivr == -1) + mivr = 4500000; + + ret = rt9455_set_field_val(info, F_MIVR, + rt9455_mivr_values, + ARRAY_SIZE(rt9455_mivr_values), mivr); + if (ret) { + dev_err(dev, "Failed to set MIVR value\n"); + return ret; + } + + /* + * Set IAICR to value retrieved from device-specific data. + * If no value is specified, default value for IAICR is 500 mA. + */ + if (iaicr == -1) + iaicr = 500000; + + ret = rt9455_set_field_val(info, F_IAICR, + rt9455_iaicr_values, + ARRAY_SIZE(rt9455_iaicr_values), iaicr); + if (ret) { + dev_err(dev, "Failed to set IAICR value\n"); + return ret; + } + + /* + * Set IAICR_INT bit so that IAICR value is determined by IAICR bits + * and not by OTG pin. + */ + ret = regmap_field_write(info->regmap_fields[F_IAICR_INT], 0x01); + if (ret) { + dev_err(dev, "Failed to set IAICR_INT bit\n"); + return ret; + } + + /* + * Disable CHMIVRI interrupt. Because the driver sets MIVR value, + * CHMIVRI is triggered, but there is no action to be taken by the + * driver when CHMIVRI is triggered. + */ + ret = regmap_field_write(info->regmap_fields[F_CHMIVRIM], 0x01); + if (ret) { + dev_err(dev, "Failed to mask CHMIVRI interrupt\n"); + return ret; + } + + return 0; +} + +#if IS_ENABLED(CONFIG_USB_PHY) +/* + * Before setting the charger into boost mode, boost output voltage is + * set. This is needed because boost output voltage may differ from battery + * regulation voltage. F_VOREG bits represent either battery regulation voltage + * or boost output voltage, depending on the mode the charger is. Both battery + * regulation voltage and boost output voltage are read from DT/ACPI during + * probe. + */ +static int rt9455_set_boost_voltage_before_boost_mode(struct rt9455_info *info) +{ + struct device *dev = &info->client->dev; + int ret; + + ret = rt9455_set_field_val(info, F_VOREG, + rt9455_boost_voltage_values, + ARRAY_SIZE(rt9455_boost_voltage_values), + info->boost_voltage); + if (ret) { + dev_err(dev, "Failed to set boost output voltage value\n"); + return ret; + } + + return 0; +} +#endif + +/* + * Before setting the charger into charge mode, battery regulation voltage is + * set. This is needed because boost output voltage may differ from battery + * regulation voltage. F_VOREG bits represent either battery regulation voltage + * or boost output voltage, depending on the mode the charger is. Both battery + * regulation voltage and boost output voltage are read from DT/ACPI during + * probe. + */ +static int rt9455_set_voreg_before_charge_mode(struct rt9455_info *info) +{ + struct device *dev = &info->client->dev; + int ret; + + ret = rt9455_set_field_val(info, F_VOREG, + rt9455_voreg_values, + ARRAY_SIZE(rt9455_voreg_values), + info->voreg); + if (ret) { + dev_err(dev, "Failed to set VOREG value\n"); + return ret; + } + + return 0; +} + +static int rt9455_irq_handler_check_irq1_register(struct rt9455_info *info, + bool *_is_battery_absent, + bool *_alert_userspace) +{ + unsigned int irq1, mask1, mask2; + struct device *dev = &info->client->dev; + bool is_battery_absent = false; + bool alert_userspace = false; + int ret; + + ret = regmap_read(info->regmap, RT9455_REG_IRQ1, &irq1); + if (ret) { + dev_err(dev, "Failed to read IRQ1 register\n"); + return ret; + } + + ret = regmap_read(info->regmap, RT9455_REG_MASK1, &mask1); + if (ret) { + dev_err(dev, "Failed to read MASK1 register\n"); + return ret; + } + + if (irq1 & GET_MASK(F_TSDI)) { + dev_err(dev, "Thermal shutdown fault occurred\n"); + alert_userspace = true; + } + + if (irq1 & GET_MASK(F_VINOVPI)) { + dev_err(dev, "Overvoltage input occurred\n"); + alert_userspace = true; + } + + if (irq1 & GET_MASK(F_BATAB)) { + dev_err(dev, "Battery absence occurred\n"); + is_battery_absent = true; + alert_userspace = true; + + if ((mask1 & GET_MASK(F_BATABM)) == 0) { + ret = regmap_field_write(info->regmap_fields[F_BATABM], + 0x01); + if (ret) { + dev_err(dev, "Failed to mask BATAB interrupt\n"); + return ret; + } + } + + ret = regmap_read(info->regmap, RT9455_REG_MASK2, &mask2); + if (ret) { + dev_err(dev, "Failed to read MASK2 register\n"); + return ret; + } + + if (mask2 & GET_MASK(F_CHTERMIM)) { + ret = regmap_field_write( + info->regmap_fields[F_CHTERMIM], 0x00); + if (ret) { + dev_err(dev, "Failed to unmask CHTERMI interrupt\n"); + return ret; + } + } + + if (mask2 & GET_MASK(F_CHRCHGIM)) { + ret = regmap_field_write( + info->regmap_fields[F_CHRCHGIM], 0x00); + if (ret) { + dev_err(dev, "Failed to unmask CHRCHGI interrupt\n"); + return ret; + } + } + + /* + * When the battery is absent, max_charging_time_work is + * cancelled, since no charging is done. + */ + cancel_delayed_work_sync(&info->max_charging_time_work); + /* + * Since no interrupt is triggered when the battery is + * reconnected, max_charging_time_work is not rescheduled. + * Therefore, batt_presence_work is scheduled to check whether + * the battery is still absent or not. + */ + queue_delayed_work(system_power_efficient_wq, + &info->batt_presence_work, + RT9455_BATT_PRESENCE_DELAY * HZ); + } + + *_is_battery_absent = is_battery_absent; + + if (alert_userspace) + *_alert_userspace = alert_userspace; + + return 0; +} + +static int rt9455_irq_handler_check_irq2_register(struct rt9455_info *info, + bool is_battery_absent, + bool *_alert_userspace) +{ + unsigned int irq2, mask2; + struct device *dev = &info->client->dev; + bool alert_userspace = false; + int ret; + + ret = regmap_read(info->regmap, RT9455_REG_IRQ2, &irq2); + if (ret) { + dev_err(dev, "Failed to read IRQ2 register\n"); + return ret; + } + + ret = regmap_read(info->regmap, RT9455_REG_MASK2, &mask2); + if (ret) { + dev_err(dev, "Failed to read MASK2 register\n"); + return ret; + } + + if (irq2 & GET_MASK(F_CHRVPI)) { + dev_dbg(dev, "Charger fault occurred\n"); + /* + * CHRVPI bit is set in 2 cases: + * 1. when the power source is connected to the charger. + * 2. when the power source is disconnected from the charger. + * To identify the case, PWR_RDY bit is checked. Because + * PWR_RDY bit is set / cleared after CHRVPI interrupt is + * triggered, it is used delayed_work to later read PWR_RDY bit. + * Also, do not set to true alert_userspace, because there is no + * need to notify userspace when CHRVPI interrupt has occurred. + * Userspace will be notified after PWR_RDY bit is read. + */ + queue_delayed_work(system_power_efficient_wq, + &info->pwr_rdy_work, + RT9455_PWR_RDY_DELAY * HZ); + } + if (irq2 & GET_MASK(F_CHBATOVI)) { + dev_err(dev, "Battery OVP occurred\n"); + alert_userspace = true; + } + if (irq2 & GET_MASK(F_CHTERMI)) { + dev_dbg(dev, "Charge terminated\n"); + if (!is_battery_absent) { + if ((mask2 & GET_MASK(F_CHTERMIM)) == 0) { + ret = regmap_field_write( + info->regmap_fields[F_CHTERMIM], 0x01); + if (ret) { + dev_err(dev, "Failed to mask CHTERMI interrupt\n"); + return ret; + } + /* + * Update MASK2 value, since CHTERMIM bit is + * set. + */ + mask2 = mask2 | GET_MASK(F_CHTERMIM); + } + cancel_delayed_work_sync(&info->max_charging_time_work); + alert_userspace = true; + } + } + if (irq2 & GET_MASK(F_CHRCHGI)) { + dev_dbg(dev, "Recharge request\n"); + ret = regmap_field_write(info->regmap_fields[F_CHG_EN], + RT9455_CHARGE_ENABLE); + if (ret) { + dev_err(dev, "Failed to enable charging\n"); + return ret; + } + if (mask2 & GET_MASK(F_CHTERMIM)) { + ret = regmap_field_write( + info->regmap_fields[F_CHTERMIM], 0x00); + if (ret) { + dev_err(dev, "Failed to unmask CHTERMI interrupt\n"); + return ret; + } + /* Update MASK2 value, since CHTERMIM bit is cleared. */ + mask2 = mask2 & ~GET_MASK(F_CHTERMIM); + } + if (!is_battery_absent) { + /* + * No need to check whether the charger is connected to + * power source when CHRCHGI is received, since CHRCHGI + * is not triggered if the charger is not connected to + * the power source. + */ + queue_delayed_work(system_power_efficient_wq, + &info->max_charging_time_work, + RT9455_MAX_CHARGING_TIME * HZ); + alert_userspace = true; + } + } + if (irq2 & GET_MASK(F_CH32MI)) { + dev_err(dev, "Charger fault. 32 mins timeout occurred\n"); + alert_userspace = true; + } + if (irq2 & GET_MASK(F_CHTREGI)) { + dev_warn(dev, + "Charger warning. Thermal regulation loop active\n"); + alert_userspace = true; + } + if (irq2 & GET_MASK(F_CHMIVRI)) { + dev_dbg(dev, + "Charger warning. Input voltage MIVR loop active\n"); + } + + if (alert_userspace) + *_alert_userspace = alert_userspace; + + return 0; +} + +static int rt9455_irq_handler_check_irq3_register(struct rt9455_info *info, + bool *_alert_userspace) +{ + unsigned int irq3, mask3; + struct device *dev = &info->client->dev; + bool alert_userspace = false; + int ret; + + ret = regmap_read(info->regmap, RT9455_REG_IRQ3, &irq3); + if (ret) { + dev_err(dev, "Failed to read IRQ3 register\n"); + return ret; + } + + ret = regmap_read(info->regmap, RT9455_REG_MASK3, &mask3); + if (ret) { + dev_err(dev, "Failed to read MASK3 register\n"); + return ret; + } + + if (irq3 & GET_MASK(F_BSTBUSOVI)) { + dev_err(dev, "Boost fault. Overvoltage input occurred\n"); + alert_userspace = true; + } + if (irq3 & GET_MASK(F_BSTOLI)) { + dev_err(dev, "Boost fault. Overload\n"); + alert_userspace = true; + } + if (irq3 & GET_MASK(F_BSTLOWVI)) { + dev_err(dev, "Boost fault. Battery voltage too low\n"); + alert_userspace = true; + } + if (irq3 & GET_MASK(F_BST32SI)) { + dev_err(dev, "Boost fault. 32 seconds timeout occurred.\n"); + alert_userspace = true; + } + + if (alert_userspace) { + dev_info(dev, "Boost fault occurred, therefore the charger goes into charge mode\n"); + ret = rt9455_set_voreg_before_charge_mode(info); + if (ret) { + dev_err(dev, "Failed to set VOREG before entering charge mode\n"); + return ret; + } + ret = regmap_field_write(info->regmap_fields[F_OPA_MODE], + RT9455_CHARGE_MODE); + if (ret) { + dev_err(dev, "Failed to set charger in charge mode\n"); + return ret; + } + *_alert_userspace = alert_userspace; + } + + return 0; +} + +static irqreturn_t rt9455_irq_handler_thread(int irq, void *data) +{ + struct rt9455_info *info = data; + struct device *dev; + bool alert_userspace = false; + bool is_battery_absent = false; + unsigned int status; + int ret; + + if (!info) + return IRQ_NONE; + + dev = &info->client->dev; + + if (irq != info->client->irq) { + dev_err(dev, "Interrupt is not for RT9455 charger\n"); + return IRQ_NONE; + } + + ret = regmap_field_read(info->regmap_fields[F_STAT], &status); + if (ret) { + dev_err(dev, "Failed to read STAT bits\n"); + return IRQ_HANDLED; + } + dev_dbg(dev, "Charger status is %d\n", status); + + /* + * Each function that processes an IRQ register receives as output + * parameter alert_userspace pointer. alert_userspace is set to true + * in such a function only if an interrupt has occurred in the + * respective interrupt register. This way, it is avoided the following + * case: interrupt occurs only in IRQ1 register, + * rt9455_irq_handler_check_irq1_register() function sets to true + * alert_userspace, but rt9455_irq_handler_check_irq2_register() + * and rt9455_irq_handler_check_irq3_register() functions set to false + * alert_userspace and power_supply_changed() is never called. + */ + ret = rt9455_irq_handler_check_irq1_register(info, &is_battery_absent, + &alert_userspace); + if (ret) { + dev_err(dev, "Failed to handle IRQ1 register\n"); + return IRQ_HANDLED; + } + + ret = rt9455_irq_handler_check_irq2_register(info, is_battery_absent, + &alert_userspace); + if (ret) { + dev_err(dev, "Failed to handle IRQ2 register\n"); + return IRQ_HANDLED; + } + + ret = rt9455_irq_handler_check_irq3_register(info, &alert_userspace); + if (ret) { + dev_err(dev, "Failed to handle IRQ3 register\n"); + return IRQ_HANDLED; + } + + if (alert_userspace) { + /* + * Sometimes, an interrupt occurs while rt9455_probe() function + * is executing and power_supply_register() is not yet called. + * Do not call power_supply_changed() in this case. + */ + if (info->charger) + power_supply_changed(info->charger); + } + + return IRQ_HANDLED; +} + +static int rt9455_discover_charger(struct rt9455_info *info, u32 *ichrg, + u32 *ieoc_percentage, + u32 *mivr, u32 *iaicr) +{ + struct device *dev = &info->client->dev; + int ret; + + if (!dev->of_node && !ACPI_HANDLE(dev)) { + dev_err(dev, "No support for either device tree or ACPI\n"); + return -EINVAL; + } + /* + * ICHRG, IEOC_PERCENTAGE, VOREG and boost output voltage are mandatory + * parameters. + */ + ret = device_property_read_u32(dev, "richtek,output-charge-current", + ichrg); + if (ret) { + dev_err(dev, "Error: missing \"output-charge-current\" property\n"); + return ret; + } + + ret = device_property_read_u32(dev, "richtek,end-of-charge-percentage", + ieoc_percentage); + if (ret) { + dev_err(dev, "Error: missing \"end-of-charge-percentage\" property\n"); + return ret; + } + + ret = device_property_read_u32(dev, + "richtek,battery-regulation-voltage", + &info->voreg); + if (ret) { + dev_err(dev, "Error: missing \"battery-regulation-voltage\" property\n"); + return ret; + } + + ret = device_property_read_u32(dev, "richtek,boost-output-voltage", + &info->boost_voltage); + if (ret) { + dev_err(dev, "Error: missing \"boost-output-voltage\" property\n"); + return ret; + } + + /* + * MIVR and IAICR are optional parameters. Do not return error if one of + * them is not present in ACPI table or device tree specification. + */ + device_property_read_u32(dev, "richtek,min-input-voltage-regulation", + mivr); + device_property_read_u32(dev, "richtek,avg-input-current-regulation", + iaicr); + + return 0; +} + +#if IS_ENABLED(CONFIG_USB_PHY) +static int rt9455_usb_event_none(struct rt9455_info *info, + u8 opa_mode, u8 iaicr) +{ + struct device *dev = &info->client->dev; + int ret; + + if (opa_mode == RT9455_BOOST_MODE) { + ret = rt9455_set_voreg_before_charge_mode(info); + if (ret) { + dev_err(dev, "Failed to set VOREG before entering charge mode\n"); + return ret; + } + /* + * If the charger is in boost mode, and it has received + * USB_EVENT_NONE, this means the consumer device powered by the + * charger is not connected anymore. + * In this case, the charger goes into charge mode. + */ + dev_dbg(dev, "USB_EVENT_NONE received, therefore the charger goes into charge mode\n"); + ret = regmap_field_write(info->regmap_fields[F_OPA_MODE], + RT9455_CHARGE_MODE); + if (ret) { + dev_err(dev, "Failed to set charger in charge mode\n"); + return NOTIFY_DONE; + } + } + + dev_dbg(dev, "USB_EVENT_NONE received, therefore IAICR is set to its minimum value\n"); + if (iaicr != RT9455_IAICR_100MA) { + ret = regmap_field_write(info->regmap_fields[F_IAICR], + RT9455_IAICR_100MA); + if (ret) { + dev_err(dev, "Failed to set IAICR value\n"); + return NOTIFY_DONE; + } + } + + return NOTIFY_OK; +} + +static int rt9455_usb_event_vbus(struct rt9455_info *info, + u8 opa_mode, u8 iaicr) +{ + struct device *dev = &info->client->dev; + int ret; + + if (opa_mode == RT9455_BOOST_MODE) { + ret = rt9455_set_voreg_before_charge_mode(info); + if (ret) { + dev_err(dev, "Failed to set VOREG before entering charge mode\n"); + return ret; + } + /* + * If the charger is in boost mode, and it has received + * USB_EVENT_VBUS, this means the consumer device powered by the + * charger is not connected anymore. + * In this case, the charger goes into charge mode. + */ + dev_dbg(dev, "USB_EVENT_VBUS received, therefore the charger goes into charge mode\n"); + ret = regmap_field_write(info->regmap_fields[F_OPA_MODE], + RT9455_CHARGE_MODE); + if (ret) { + dev_err(dev, "Failed to set charger in charge mode\n"); + return NOTIFY_DONE; + } + } + + dev_dbg(dev, "USB_EVENT_VBUS received, therefore IAICR is set to 500 mA\n"); + if (iaicr != RT9455_IAICR_500MA) { + ret = regmap_field_write(info->regmap_fields[F_IAICR], + RT9455_IAICR_500MA); + if (ret) { + dev_err(dev, "Failed to set IAICR value\n"); + return NOTIFY_DONE; + } + } + + return NOTIFY_OK; +} + +static int rt9455_usb_event_id(struct rt9455_info *info, + u8 opa_mode, u8 iaicr) +{ + struct device *dev = &info->client->dev; + int ret; + + if (opa_mode == RT9455_CHARGE_MODE) { + ret = rt9455_set_boost_voltage_before_boost_mode(info); + if (ret) { + dev_err(dev, "Failed to set boost output voltage before entering boost mode\n"); + return ret; + } + /* + * If the charger is in charge mode, and it has received + * USB_EVENT_ID, this means a consumer device is connected and + * it should be powered by the charger. + * In this case, the charger goes into boost mode. + */ + dev_dbg(dev, "USB_EVENT_ID received, therefore the charger goes into boost mode\n"); + ret = regmap_field_write(info->regmap_fields[F_OPA_MODE], + RT9455_BOOST_MODE); + if (ret) { + dev_err(dev, "Failed to set charger in boost mode\n"); + return NOTIFY_DONE; + } + } + + dev_dbg(dev, "USB_EVENT_ID received, therefore IAICR is set to its minimum value\n"); + if (iaicr != RT9455_IAICR_100MA) { + ret = regmap_field_write(info->regmap_fields[F_IAICR], + RT9455_IAICR_100MA); + if (ret) { + dev_err(dev, "Failed to set IAICR value\n"); + return NOTIFY_DONE; + } + } + + return NOTIFY_OK; +} + +static int rt9455_usb_event_charger(struct rt9455_info *info, + u8 opa_mode, u8 iaicr) +{ + struct device *dev = &info->client->dev; + int ret; + + if (opa_mode == RT9455_BOOST_MODE) { + ret = rt9455_set_voreg_before_charge_mode(info); + if (ret) { + dev_err(dev, "Failed to set VOREG before entering charge mode\n"); + return ret; + } + /* + * If the charger is in boost mode, and it has received + * USB_EVENT_CHARGER, this means the consumer device powered by + * the charger is not connected anymore. + * In this case, the charger goes into charge mode. + */ + dev_dbg(dev, "USB_EVENT_CHARGER received, therefore the charger goes into charge mode\n"); + ret = regmap_field_write(info->regmap_fields[F_OPA_MODE], + RT9455_CHARGE_MODE); + if (ret) { + dev_err(dev, "Failed to set charger in charge mode\n"); + return NOTIFY_DONE; + } + } + + dev_dbg(dev, "USB_EVENT_CHARGER received, therefore IAICR is set to no current limit\n"); + if (iaicr != RT9455_IAICR_NO_LIMIT) { + ret = regmap_field_write(info->regmap_fields[F_IAICR], + RT9455_IAICR_NO_LIMIT); + if (ret) { + dev_err(dev, "Failed to set IAICR value\n"); + return NOTIFY_DONE; + } + } + + return NOTIFY_OK; +} + +static int rt9455_usb_event(struct notifier_block *nb, + unsigned long event, void *power) +{ + struct rt9455_info *info = container_of(nb, struct rt9455_info, nb); + struct device *dev = &info->client->dev; + unsigned int opa_mode, iaicr; + int ret; + + /* + * Determine whether the charger is in charge mode + * or in boost mode. + */ + ret = regmap_field_read(info->regmap_fields[F_OPA_MODE], + &opa_mode); + if (ret) { + dev_err(dev, "Failed to read OPA_MODE value\n"); + return NOTIFY_DONE; + } + + ret = regmap_field_read(info->regmap_fields[F_IAICR], + &iaicr); + if (ret) { + dev_err(dev, "Failed to read IAICR value\n"); + return NOTIFY_DONE; + } + + dev_dbg(dev, "Received USB event %lu\n", event); + switch (event) { + case USB_EVENT_NONE: + return rt9455_usb_event_none(info, opa_mode, iaicr); + case USB_EVENT_VBUS: + return rt9455_usb_event_vbus(info, opa_mode, iaicr); + case USB_EVENT_ID: + return rt9455_usb_event_id(info, opa_mode, iaicr); + case USB_EVENT_CHARGER: + return rt9455_usb_event_charger(info, opa_mode, iaicr); + default: + dev_err(dev, "Unknown USB event\n"); + } + return NOTIFY_DONE; +} +#endif + +static void rt9455_pwr_rdy_work_callback(struct work_struct *work) +{ + struct rt9455_info *info = container_of(work, struct rt9455_info, + pwr_rdy_work.work); + struct device *dev = &info->client->dev; + unsigned int pwr_rdy; + int ret; + + ret = regmap_field_read(info->regmap_fields[F_PWR_RDY], &pwr_rdy); + if (ret) { + dev_err(dev, "Failed to read PWR_RDY bit\n"); + return; + } + switch (pwr_rdy) { + case RT9455_PWR_FAULT: + dev_dbg(dev, "Charger disconnected from power source\n"); + cancel_delayed_work_sync(&info->max_charging_time_work); + break; + case RT9455_PWR_GOOD: + dev_dbg(dev, "Charger connected to power source\n"); + ret = regmap_field_write(info->regmap_fields[F_CHG_EN], + RT9455_CHARGE_ENABLE); + if (ret) { + dev_err(dev, "Failed to enable charging\n"); + return; + } + queue_delayed_work(system_power_efficient_wq, + &info->max_charging_time_work, + RT9455_MAX_CHARGING_TIME * HZ); + break; + } + /* + * Notify userspace that the charger has been either connected to or + * disconnected from the power source. + */ + power_supply_changed(info->charger); +} + +static void rt9455_max_charging_time_work_callback(struct work_struct *work) +{ + struct rt9455_info *info = container_of(work, struct rt9455_info, + max_charging_time_work.work); + struct device *dev = &info->client->dev; + int ret; + + dev_err(dev, "Battery has been charging for at least 6 hours and is not yet fully charged. Battery is dead, therefore charging is disabled.\n"); + ret = regmap_field_write(info->regmap_fields[F_CHG_EN], + RT9455_CHARGE_DISABLE); + if (ret) + dev_err(dev, "Failed to disable charging\n"); +} + +static void rt9455_batt_presence_work_callback(struct work_struct *work) +{ + struct rt9455_info *info = container_of(work, struct rt9455_info, + batt_presence_work.work); + struct device *dev = &info->client->dev; + unsigned int irq1, mask1; + int ret; + + ret = regmap_read(info->regmap, RT9455_REG_IRQ1, &irq1); + if (ret) { + dev_err(dev, "Failed to read IRQ1 register\n"); + return; + } + + /* + * If the battery is still absent, batt_presence_work is rescheduled. + * Otherwise, max_charging_time is scheduled. + */ + if (irq1 & GET_MASK(F_BATAB)) { + queue_delayed_work(system_power_efficient_wq, + &info->batt_presence_work, + RT9455_BATT_PRESENCE_DELAY * HZ); + } else { + queue_delayed_work(system_power_efficient_wq, + &info->max_charging_time_work, + RT9455_MAX_CHARGING_TIME * HZ); + + ret = regmap_read(info->regmap, RT9455_REG_MASK1, &mask1); + if (ret) { + dev_err(dev, "Failed to read MASK1 register\n"); + return; + } + + if (mask1 & GET_MASK(F_BATABM)) { + ret = regmap_field_write(info->regmap_fields[F_BATABM], + 0x00); + if (ret) + dev_err(dev, "Failed to unmask BATAB interrupt\n"); + } + /* + * Notify userspace that the battery is now connected to the + * charger. + */ + power_supply_changed(info->charger); + } +} + +static const struct power_supply_desc rt9455_charger_desc = { + .name = RT9455_DRIVER_NAME, + .type = POWER_SUPPLY_TYPE_USB, + .properties = rt9455_charger_properties, + .num_properties = ARRAY_SIZE(rt9455_charger_properties), + .get_property = rt9455_charger_get_property, +}; + +static bool rt9455_is_writeable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case RT9455_REG_DEV_ID: + case RT9455_REG_IRQ1: + case RT9455_REG_IRQ2: + case RT9455_REG_IRQ3: + return false; + default: + return true; + } +} + +static bool rt9455_is_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case RT9455_REG_DEV_ID: + case RT9455_REG_CTRL5: + case RT9455_REG_CTRL6: + return false; + default: + return true; + } +} + +static const struct regmap_config rt9455_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .writeable_reg = rt9455_is_writeable_reg, + .volatile_reg = rt9455_is_volatile_reg, + .max_register = RT9455_REG_MASK3, + .cache_type = REGCACHE_RBTREE, +}; + +static int rt9455_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct device *dev = &client->dev; + struct rt9455_info *info; + struct power_supply_config rt9455_charger_config = {}; + /* + * Mandatory device-specific data values. Also, VOREG and boost output + * voltage are mandatory values, but they are stored in rt9455_info + * structure. + */ + u32 ichrg, ieoc_percentage; + /* Optional device-specific data values. */ + u32 mivr = -1, iaicr = -1; + int i, ret; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { + dev_err(dev, "No support for SMBUS_BYTE_DATA\n"); + return -ENODEV; + } + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->client = client; + i2c_set_clientdata(client, info); + + info->regmap = devm_regmap_init_i2c(client, + &rt9455_regmap_config); + if (IS_ERR(info->regmap)) { + dev_err(dev, "Failed to initialize register map\n"); + return -EINVAL; + } + + for (i = 0; i < F_MAX_FIELDS; i++) { + info->regmap_fields[i] = + devm_regmap_field_alloc(dev, info->regmap, + rt9455_reg_fields[i]); + if (IS_ERR(info->regmap_fields[i])) { + dev_err(dev, + "Failed to allocate regmap field = %d\n", i); + return PTR_ERR(info->regmap_fields[i]); + } + } + + ret = rt9455_discover_charger(info, &ichrg, &ieoc_percentage, + &mivr, &iaicr); + if (ret) { + dev_err(dev, "Failed to discover charger\n"); + return ret; + } + +#if IS_ENABLED(CONFIG_USB_PHY) + info->usb_phy = devm_usb_get_phy(dev, USB_PHY_TYPE_USB2); + if (IS_ERR(info->usb_phy)) { + dev_err(dev, "Failed to get USB transceiver\n"); + } else { + info->nb.notifier_call = rt9455_usb_event; + ret = usb_register_notifier(info->usb_phy, &info->nb); + if (ret) { + dev_err(dev, "Failed to register USB notifier\n"); + /* + * If usb_register_notifier() fails, set notifier_call + * to NULL, to avoid calling usb_unregister_notifier(). + */ + info->nb.notifier_call = NULL; + } + } +#endif + + INIT_DEFERRABLE_WORK(&info->pwr_rdy_work, rt9455_pwr_rdy_work_callback); + INIT_DEFERRABLE_WORK(&info->max_charging_time_work, + rt9455_max_charging_time_work_callback); + INIT_DEFERRABLE_WORK(&info->batt_presence_work, + rt9455_batt_presence_work_callback); + + rt9455_charger_config.of_node = dev->of_node; + rt9455_charger_config.drv_data = info; + rt9455_charger_config.supplied_to = rt9455_charger_supplied_to; + rt9455_charger_config.num_supplicants = + ARRAY_SIZE(rt9455_charger_supplied_to); + ret = devm_request_threaded_irq(dev, client->irq, NULL, + rt9455_irq_handler_thread, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + RT9455_DRIVER_NAME, info); + if (ret) { + dev_err(dev, "Failed to register IRQ handler\n"); + goto put_usb_notifier; + } + + ret = rt9455_hw_init(info, ichrg, ieoc_percentage, mivr, iaicr); + if (ret) { + dev_err(dev, "Failed to set charger to its default values\n"); + goto put_usb_notifier; + } + + info->charger = devm_power_supply_register(dev, &rt9455_charger_desc, + &rt9455_charger_config); + if (IS_ERR(info->charger)) { + dev_err(dev, "Failed to register charger\n"); + ret = PTR_ERR(info->charger); + goto put_usb_notifier; + } + + return 0; + +put_usb_notifier: +#if IS_ENABLED(CONFIG_USB_PHY) + if (info->nb.notifier_call) { + usb_unregister_notifier(info->usb_phy, &info->nb); + info->nb.notifier_call = NULL; + } +#endif + return ret; +} + +static int rt9455_remove(struct i2c_client *client) +{ + int ret; + struct rt9455_info *info = i2c_get_clientdata(client); + + ret = rt9455_register_reset(info); + if (ret) + dev_err(&info->client->dev, "Failed to set charger to its default values\n"); + +#if IS_ENABLED(CONFIG_USB_PHY) + if (info->nb.notifier_call) + usb_unregister_notifier(info->usb_phy, &info->nb); +#endif + + cancel_delayed_work_sync(&info->pwr_rdy_work); + cancel_delayed_work_sync(&info->max_charging_time_work); + cancel_delayed_work_sync(&info->batt_presence_work); + + return ret; +} + +static const struct i2c_device_id rt9455_i2c_id_table[] = { + { RT9455_DRIVER_NAME, 0 }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, rt9455_i2c_id_table); + +static const struct of_device_id rt9455_of_match[] = { + { .compatible = "richtek,rt9455", }, + { }, +}; +MODULE_DEVICE_TABLE(of, rt9455_of_match); + +#ifdef CONFIG_ACPI +static const struct acpi_device_id rt9455_i2c_acpi_match[] = { + { "RT945500", 0 }, + { } +}; +MODULE_DEVICE_TABLE(acpi, rt9455_i2c_acpi_match); +#endif + +static struct i2c_driver rt9455_driver = { + .probe = rt9455_probe, + .remove = rt9455_remove, + .id_table = rt9455_i2c_id_table, + .driver = { + .name = RT9455_DRIVER_NAME, + .of_match_table = of_match_ptr(rt9455_of_match), + .acpi_match_table = ACPI_PTR(rt9455_i2c_acpi_match), + }, +}; +module_i2c_driver(rt9455_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Anda-Maria Nicolae <anda-maria.nicolae@intel.com>"); +MODULE_DESCRIPTION("Richtek RT9455 Charger Driver"); diff --git a/drivers/power/supply/rx51_battery.c b/drivers/power/supply/rx51_battery.c new file mode 100644 index 000000000..6e488ecf4 --- /dev/null +++ b/drivers/power/supply/rx51_battery.c @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Nokia RX-51 battery driver + * + * Copyright (C) 2012 Pali Rohár <pali@kernel.org> + */ + +#include <linux/module.h> +#include <linux/param.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/iio/consumer.h> +#include <linux/of.h> + +struct rx51_device_info { + struct device *dev; + struct power_supply *bat; + struct power_supply_desc bat_desc; + struct iio_channel *channel_temp; + struct iio_channel *channel_bsi; + struct iio_channel *channel_vbat; +}; + +/* + * Read ADCIN channel value, code copied from maemo kernel + */ +static int rx51_battery_read_adc(struct iio_channel *channel) +{ + int val, err; + err = iio_read_channel_average_raw(channel, &val); + if (err < 0) + return err; + return val; +} + +/* + * Read ADCIN channel 12 (voltage) and convert RAW value to micro voltage + * This conversion formula was extracted from maemo program bsi-read + */ +static int rx51_battery_read_voltage(struct rx51_device_info *di) +{ + int voltage = rx51_battery_read_adc(di->channel_vbat); + + if (voltage < 0) { + dev_err(di->dev, "Could not read ADC: %d\n", voltage); + return voltage; + } + + return 1000 * (10000 * voltage / 1705); +} + +/* + * Temperature look-up tables + * TEMP = (1/(t1 + 1/298) - 273.15) + * Where t1 = (1/B) * ln((RAW_ADC_U * 2.5)/(R * I * 255)) + * Formula is based on experimental data, RX-51 CAL data, maemo program bme + * and formula from da9052 driver with values R = 100, B = 3380, I = 0.00671 + */ + +/* + * Table1 (temperature for first 25 RAW values) + * Usage: TEMP = rx51_temp_table1[RAW] + * RAW is between 1 and 24 + * TEMP is between 201 C and 55 C + */ +static u8 rx51_temp_table1[] = { + 255, 201, 159, 138, 124, 114, 106, 99, 94, 89, 85, 82, 78, 75, + 73, 70, 68, 66, 64, 62, 61, 59, 57, 56, 55 +}; + +/* + * Table2 (lowest RAW value for temperature) + * Usage: RAW = rx51_temp_table2[TEMP-rx51_temp_table2_first] + * TEMP is between 53 C and -32 C + * RAW is between 25 and 993 + */ +#define rx51_temp_table2_first 53 +static u16 rx51_temp_table2[] = { + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 39, + 40, 41, 43, 44, 46, 48, 49, 51, 53, 55, 57, 59, 61, 64, + 66, 69, 71, 74, 77, 80, 83, 86, 90, 94, 97, 101, 106, 110, + 115, 119, 125, 130, 136, 141, 148, 154, 161, 168, 176, 184, 202, 211, + 221, 231, 242, 254, 266, 279, 293, 308, 323, 340, 357, 375, 395, 415, + 437, 460, 485, 511, 539, 568, 600, 633, 669, 706, 747, 790, 836, 885, + 937, 993, 1024 +}; + +/* + * Read ADCIN channel 0 (battery temp) and convert value to tenths of Celsius + * Use Temperature look-up tables for conversation + */ +static int rx51_battery_read_temperature(struct rx51_device_info *di) +{ + int min = 0; + int max = ARRAY_SIZE(rx51_temp_table2) - 1; + int raw = rx51_battery_read_adc(di->channel_temp); + + if (raw < 0) + dev_err(di->dev, "Could not read ADC: %d\n", raw); + + /* Zero and negative values are undefined */ + if (raw <= 0) + return INT_MAX; + + /* ADC channels are 10 bit, higher value are undefined */ + if (raw >= (1 << 10)) + return INT_MIN; + + /* First check for temperature in first direct table */ + if (raw < ARRAY_SIZE(rx51_temp_table1)) + return rx51_temp_table1[raw] * 10; + + /* Binary search RAW value in second inverse table */ + while (max - min > 1) { + int mid = (max + min) / 2; + if (rx51_temp_table2[mid] <= raw) + min = mid; + else if (rx51_temp_table2[mid] > raw) + max = mid; + if (rx51_temp_table2[mid] == raw) + break; + } + + return (rx51_temp_table2_first - min) * 10; +} + +/* + * Read ADCIN channel 4 (BSI) and convert RAW value to micro Ah + * This conversion formula was extracted from maemo program bsi-read + */ +static int rx51_battery_read_capacity(struct rx51_device_info *di) +{ + int capacity = rx51_battery_read_adc(di->channel_bsi); + + if (capacity < 0) { + dev_err(di->dev, "Could not read ADC: %d\n", capacity); + return capacity; + } + + return 1280 * (1200 * capacity)/(1024 - capacity); +} + +/* + * Return power_supply property + */ +static int rx51_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct rx51_device_info *di = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = 4200000; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = rx51_battery_read_voltage(di) ? 1 : 0; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = rx51_battery_read_voltage(di); + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = rx51_battery_read_temperature(di); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = rx51_battery_read_capacity(di); + break; + default: + return -EINVAL; + } + + if (val->intval == INT_MAX || val->intval == INT_MIN) + return -EINVAL; + + return 0; +} + +static enum power_supply_property rx51_battery_props[] = { + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, +}; + +static int rx51_battery_probe(struct platform_device *pdev) +{ + struct power_supply_config psy_cfg = {}; + struct rx51_device_info *di; + int ret; + + di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); + if (!di) + return -ENOMEM; + + platform_set_drvdata(pdev, di); + + di->dev = &pdev->dev; + di->bat_desc.name = "rx51-battery"; + di->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY; + di->bat_desc.properties = rx51_battery_props; + di->bat_desc.num_properties = ARRAY_SIZE(rx51_battery_props); + di->bat_desc.get_property = rx51_battery_get_property; + + psy_cfg.drv_data = di; + + di->channel_temp = iio_channel_get(di->dev, "temp"); + if (IS_ERR(di->channel_temp)) { + ret = PTR_ERR(di->channel_temp); + goto error; + } + + di->channel_bsi = iio_channel_get(di->dev, "bsi"); + if (IS_ERR(di->channel_bsi)) { + ret = PTR_ERR(di->channel_bsi); + goto error_channel_temp; + } + + di->channel_vbat = iio_channel_get(di->dev, "vbat"); + if (IS_ERR(di->channel_vbat)) { + ret = PTR_ERR(di->channel_vbat); + goto error_channel_bsi; + } + + di->bat = power_supply_register(di->dev, &di->bat_desc, &psy_cfg); + if (IS_ERR(di->bat)) { + ret = PTR_ERR(di->bat); + goto error_channel_vbat; + } + + return 0; + +error_channel_vbat: + iio_channel_release(di->channel_vbat); +error_channel_bsi: + iio_channel_release(di->channel_bsi); +error_channel_temp: + iio_channel_release(di->channel_temp); +error: + + return ret; +} + +static int rx51_battery_remove(struct platform_device *pdev) +{ + struct rx51_device_info *di = platform_get_drvdata(pdev); + + power_supply_unregister(di->bat); + + iio_channel_release(di->channel_vbat); + iio_channel_release(di->channel_bsi); + iio_channel_release(di->channel_temp); + + return 0; +} + +#ifdef CONFIG_OF +static const struct of_device_id n900_battery_of_match[] = { + {.compatible = "nokia,n900-battery", }, + { }, +}; +MODULE_DEVICE_TABLE(of, n900_battery_of_match); +#endif + +static struct platform_driver rx51_battery_driver = { + .probe = rx51_battery_probe, + .remove = rx51_battery_remove, + .driver = { + .name = "rx51-battery", + .of_match_table = of_match_ptr(n900_battery_of_match), + }, +}; +module_platform_driver(rx51_battery_driver); + +MODULE_ALIAS("platform:rx51-battery"); +MODULE_AUTHOR("Pali Rohár <pali@kernel.org>"); +MODULE_DESCRIPTION("Nokia RX-51 battery driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/s3c_adc_battery.c b/drivers/power/supply/s3c_adc_battery.c new file mode 100644 index 000000000..ff46bcf5d --- /dev/null +++ b/drivers/power/supply/s3c_adc_battery.c @@ -0,0 +1,461 @@ +/* + * iPAQ h1930/h1940/rx1950 battery controller driver + * Copyright (c) Vasily Khoruzhick + * Based on h1940_battery.c by Arnaud Patard + * + * This file is subject to the terms and conditions of the GNU General Public + * License. See the file COPYING in the main directory of this archive for + * more details. + * + */ + +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/leds.h> +#include <linux/gpio.h> +#include <linux/err.h> +#include <linux/timer.h> +#include <linux/jiffies.h> +#include <linux/s3c_adc_battery.h> +#include <linux/errno.h> +#include <linux/init.h> +#include <linux/module.h> + +#include <linux/soc/samsung/s3c-adc.h> + +#define BAT_POLL_INTERVAL 10000 /* ms */ +#define JITTER_DELAY 500 /* ms */ + +struct s3c_adc_bat { + struct power_supply *psy; + struct s3c_adc_client *client; + struct s3c_adc_bat_pdata *pdata; + int volt_value; + int cur_value; + unsigned int timestamp; + int level; + int status; + int cable_plugged:1; +}; + +static struct delayed_work bat_work; + +static void s3c_adc_bat_ext_power_changed(struct power_supply *psy) +{ + schedule_delayed_work(&bat_work, + msecs_to_jiffies(JITTER_DELAY)); +} + +static int gather_samples(struct s3c_adc_client *client, int num, int channel) +{ + int value, i; + + /* default to 1 if nothing is set */ + if (num < 1) + num = 1; + + value = 0; + for (i = 0; i < num; i++) + value += s3c_adc_read(client, channel); + value /= num; + + return value; +} + +static enum power_supply_property s3c_adc_backup_bat_props[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, +}; + +static int s3c_adc_backup_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct s3c_adc_bat *bat = power_supply_get_drvdata(psy); + + if (!bat) { + dev_err(&psy->dev, "%s: no battery infos ?!\n", __func__); + return -EINVAL; + } + + if (bat->volt_value < 0 || + jiffies_to_msecs(jiffies - bat->timestamp) > + BAT_POLL_INTERVAL) { + bat->volt_value = gather_samples(bat->client, + bat->pdata->backup_volt_samples, + bat->pdata->backup_volt_channel); + bat->volt_value *= bat->pdata->backup_volt_mult; + bat->timestamp = jiffies; + } + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = bat->volt_value; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + val->intval = bat->pdata->backup_volt_min; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = bat->pdata->backup_volt_max; + return 0; + default: + return -EINVAL; + } +} + +static const struct power_supply_desc backup_bat_desc = { + .name = "backup-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = s3c_adc_backup_bat_props, + .num_properties = ARRAY_SIZE(s3c_adc_backup_bat_props), + .get_property = s3c_adc_backup_bat_get_property, + .use_for_apm = 1, +}; + +static struct s3c_adc_bat backup_bat; + +static enum power_supply_property s3c_adc_main_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static int calc_full_volt(int volt_val, int cur_val, int impedance) +{ + return volt_val + cur_val * impedance / 1000; +} + +static int charge_finished(struct s3c_adc_bat *bat) +{ + return bat->pdata->gpio_inverted ? + !gpio_get_value(bat->pdata->gpio_charge_finished) : + gpio_get_value(bat->pdata->gpio_charge_finished); +} + +static int s3c_adc_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct s3c_adc_bat *bat = power_supply_get_drvdata(psy); + + int new_level; + int full_volt; + const struct s3c_adc_bat_thresh *lut; + unsigned int lut_size; + + if (!bat) { + dev_err(&psy->dev, "no battery infos ?!\n"); + return -EINVAL; + } + + lut = bat->pdata->lut_noac; + lut_size = bat->pdata->lut_noac_cnt; + + if (bat->volt_value < 0 || bat->cur_value < 0 || + jiffies_to_msecs(jiffies - bat->timestamp) > + BAT_POLL_INTERVAL) { + bat->volt_value = gather_samples(bat->client, + bat->pdata->volt_samples, + bat->pdata->volt_channel) * bat->pdata->volt_mult; + bat->cur_value = gather_samples(bat->client, + bat->pdata->current_samples, + bat->pdata->current_channel) * bat->pdata->current_mult; + bat->timestamp = jiffies; + } + + if (bat->cable_plugged && + ((bat->pdata->gpio_charge_finished < 0) || + !charge_finished(bat))) { + lut = bat->pdata->lut_acin; + lut_size = bat->pdata->lut_acin_cnt; + } + + new_level = 100000; + full_volt = calc_full_volt((bat->volt_value / 1000), + (bat->cur_value / 1000), bat->pdata->internal_impedance); + + if (full_volt < calc_full_volt(lut->volt, lut->cur, + bat->pdata->internal_impedance)) { + lut_size--; + while (lut_size--) { + int lut_volt1; + int lut_volt2; + + lut_volt1 = calc_full_volt(lut[0].volt, lut[0].cur, + bat->pdata->internal_impedance); + lut_volt2 = calc_full_volt(lut[1].volt, lut[1].cur, + bat->pdata->internal_impedance); + if (full_volt < lut_volt1 && full_volt >= lut_volt2) { + new_level = (lut[1].level + + (lut[0].level - lut[1].level) * + (full_volt - lut_volt2) / + (lut_volt1 - lut_volt2)) * 1000; + break; + } + new_level = lut[1].level * 1000; + lut++; + } + } + + bat->level = new_level; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (bat->pdata->gpio_charge_finished < 0) + val->intval = bat->level == 100000 ? + POWER_SUPPLY_STATUS_FULL : bat->status; + else + val->intval = bat->status; + return 0; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = 100000; + return 0; + case POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN: + val->intval = 0; + return 0; + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = bat->level; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = bat->volt_value; + return 0; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = bat->cur_value; + return 0; + default: + return -EINVAL; + } +} + +static const struct power_supply_desc main_bat_desc = { + .name = "main-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = s3c_adc_main_bat_props, + .num_properties = ARRAY_SIZE(s3c_adc_main_bat_props), + .get_property = s3c_adc_bat_get_property, + .external_power_changed = s3c_adc_bat_ext_power_changed, + .use_for_apm = 1, +}; + +static struct s3c_adc_bat main_bat; + +static void s3c_adc_bat_work(struct work_struct *work) +{ + struct s3c_adc_bat *bat = &main_bat; + int is_charged; + int is_plugged; + static int was_plugged; + + is_plugged = power_supply_am_i_supplied(bat->psy); + bat->cable_plugged = is_plugged; + if (is_plugged != was_plugged) { + was_plugged = is_plugged; + if (is_plugged) { + if (bat->pdata->enable_charger) + bat->pdata->enable_charger(); + bat->status = POWER_SUPPLY_STATUS_CHARGING; + } else { + if (bat->pdata->disable_charger) + bat->pdata->disable_charger(); + bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + } + } else { + if ((bat->pdata->gpio_charge_finished >= 0) && is_plugged) { + is_charged = charge_finished(&main_bat); + if (is_charged) { + if (bat->pdata->disable_charger) + bat->pdata->disable_charger(); + bat->status = POWER_SUPPLY_STATUS_FULL; + } else { + if (bat->pdata->enable_charger) + bat->pdata->enable_charger(); + bat->status = POWER_SUPPLY_STATUS_CHARGING; + } + } + } + + power_supply_changed(bat->psy); +} + +static irqreturn_t s3c_adc_bat_charged(int irq, void *dev_id) +{ + schedule_delayed_work(&bat_work, + msecs_to_jiffies(JITTER_DELAY)); + return IRQ_HANDLED; +} + +static int s3c_adc_bat_probe(struct platform_device *pdev) +{ + struct s3c_adc_client *client; + struct s3c_adc_bat_pdata *pdata = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + int ret; + + client = s3c_adc_register(pdev, NULL, NULL, 0); + if (IS_ERR(client)) { + dev_err(&pdev->dev, "cannot register adc\n"); + return PTR_ERR(client); + } + + platform_set_drvdata(pdev, client); + + main_bat.client = client; + main_bat.pdata = pdata; + main_bat.volt_value = -1; + main_bat.cur_value = -1; + main_bat.cable_plugged = 0; + main_bat.status = POWER_SUPPLY_STATUS_DISCHARGING; + psy_cfg.drv_data = &main_bat; + + main_bat.psy = power_supply_register(&pdev->dev, &main_bat_desc, &psy_cfg); + if (IS_ERR(main_bat.psy)) { + ret = PTR_ERR(main_bat.psy); + goto err_reg_main; + } + if (pdata->backup_volt_mult) { + const struct power_supply_config backup_psy_cfg + = { .drv_data = &backup_bat, }; + + backup_bat.client = client; + backup_bat.pdata = pdev->dev.platform_data; + backup_bat.volt_value = -1; + backup_bat.psy = power_supply_register(&pdev->dev, + &backup_bat_desc, + &backup_psy_cfg); + if (IS_ERR(backup_bat.psy)) { + ret = PTR_ERR(backup_bat.psy); + goto err_reg_backup; + } + } + + INIT_DELAYED_WORK(&bat_work, s3c_adc_bat_work); + + if (pdata->gpio_charge_finished >= 0) { + ret = gpio_request(pdata->gpio_charge_finished, "charged"); + if (ret) + goto err_gpio; + + ret = request_irq(gpio_to_irq(pdata->gpio_charge_finished), + s3c_adc_bat_charged, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "battery charged", NULL); + if (ret) + goto err_irq; + } + + if (pdata->init) { + ret = pdata->init(); + if (ret) + goto err_platform; + } + + dev_info(&pdev->dev, "successfully loaded\n"); + device_init_wakeup(&pdev->dev, 1); + + /* Schedule timer to check current status */ + schedule_delayed_work(&bat_work, + msecs_to_jiffies(JITTER_DELAY)); + + return 0; + +err_platform: + if (pdata->gpio_charge_finished >= 0) + free_irq(gpio_to_irq(pdata->gpio_charge_finished), NULL); +err_irq: + if (pdata->gpio_charge_finished >= 0) + gpio_free(pdata->gpio_charge_finished); +err_gpio: + if (pdata->backup_volt_mult) + power_supply_unregister(backup_bat.psy); +err_reg_backup: + power_supply_unregister(main_bat.psy); +err_reg_main: + return ret; +} + +static int s3c_adc_bat_remove(struct platform_device *pdev) +{ + struct s3c_adc_client *client = platform_get_drvdata(pdev); + struct s3c_adc_bat_pdata *pdata = pdev->dev.platform_data; + + power_supply_unregister(main_bat.psy); + if (pdata->backup_volt_mult) + power_supply_unregister(backup_bat.psy); + + s3c_adc_release(client); + + if (pdata->gpio_charge_finished >= 0) { + free_irq(gpio_to_irq(pdata->gpio_charge_finished), NULL); + gpio_free(pdata->gpio_charge_finished); + } + + cancel_delayed_work_sync(&bat_work); + + if (pdata->exit) + pdata->exit(); + + return 0; +} + +#ifdef CONFIG_PM +static int s3c_adc_bat_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct s3c_adc_bat_pdata *pdata = pdev->dev.platform_data; + + if (pdata->gpio_charge_finished >= 0) { + if (device_may_wakeup(&pdev->dev)) + enable_irq_wake( + gpio_to_irq(pdata->gpio_charge_finished)); + else { + disable_irq(gpio_to_irq(pdata->gpio_charge_finished)); + main_bat.pdata->disable_charger(); + } + } + + return 0; +} + +static int s3c_adc_bat_resume(struct platform_device *pdev) +{ + struct s3c_adc_bat_pdata *pdata = pdev->dev.platform_data; + + if (pdata->gpio_charge_finished >= 0) { + if (device_may_wakeup(&pdev->dev)) + disable_irq_wake( + gpio_to_irq(pdata->gpio_charge_finished)); + else + enable_irq(gpio_to_irq(pdata->gpio_charge_finished)); + } + + /* Schedule timer to check current status */ + schedule_delayed_work(&bat_work, + msecs_to_jiffies(JITTER_DELAY)); + + return 0; +} +#else +#define s3c_adc_bat_suspend NULL +#define s3c_adc_bat_resume NULL +#endif + +static struct platform_driver s3c_adc_bat_driver = { + .driver = { + .name = "s3c-adc-battery", + }, + .probe = s3c_adc_bat_probe, + .remove = s3c_adc_bat_remove, + .suspend = s3c_adc_bat_suspend, + .resume = s3c_adc_bat_resume, +}; + +module_platform_driver(s3c_adc_bat_driver); + +MODULE_AUTHOR("Vasily Khoruzhick <anarsoul@gmail.com>"); +MODULE_DESCRIPTION("iPAQ H1930/H1940/RX1950 battery controller driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/sbs-battery.c b/drivers/power/supply/sbs-battery.c new file mode 100644 index 000000000..b6a538ebb --- /dev/null +++ b/drivers/power/supply/sbs-battery.c @@ -0,0 +1,1267 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gas Gauge driver for SBS Compliant Batteries + * + * Copyright (c) 2010, NVIDIA Corporation. + */ + +#include <linux/bits.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/of_device.h> +#include <linux/power/sbs-battery.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/stat.h> + +enum { + REG_MANUFACTURER_DATA, + REG_BATTERY_MODE, + REG_TEMPERATURE, + REG_VOLTAGE, + REG_CURRENT_NOW, + REG_CURRENT_AVG, + REG_MAX_ERR, + REG_CAPACITY, + REG_TIME_TO_EMPTY, + REG_TIME_TO_FULL, + REG_STATUS, + REG_CAPACITY_LEVEL, + REG_CYCLE_COUNT, + REG_SERIAL_NUMBER, + REG_REMAINING_CAPACITY, + REG_REMAINING_CAPACITY_CHARGE, + REG_FULL_CHARGE_CAPACITY, + REG_FULL_CHARGE_CAPACITY_CHARGE, + REG_DESIGN_CAPACITY, + REG_DESIGN_CAPACITY_CHARGE, + REG_DESIGN_VOLTAGE_MIN, + REG_DESIGN_VOLTAGE_MAX, + REG_CHEMISTRY, + REG_MANUFACTURER, + REG_MODEL_NAME, + REG_CHARGE_CURRENT, + REG_CHARGE_VOLTAGE, +}; + +#define REG_ADDR_SPEC_INFO 0x1A +#define SPEC_INFO_VERSION_MASK GENMASK(7, 4) +#define SPEC_INFO_VERSION_SHIFT 4 + +#define SBS_VERSION_1_0 1 +#define SBS_VERSION_1_1 2 +#define SBS_VERSION_1_1_WITH_PEC 3 + +#define REG_ADDR_MANUFACTURE_DATE 0x1B + +/* Battery Mode defines */ +#define BATTERY_MODE_OFFSET 0x03 +#define BATTERY_MODE_CAPACITY_MASK BIT(15) +enum sbs_capacity_mode { + CAPACITY_MODE_AMPS = 0, + CAPACITY_MODE_WATTS = BATTERY_MODE_CAPACITY_MASK +}; +#define BATTERY_MODE_CHARGER_MASK (1<<14) + +/* manufacturer access defines */ +#define MANUFACTURER_ACCESS_STATUS 0x0006 +#define MANUFACTURER_ACCESS_SLEEP 0x0011 + +/* battery status value bits */ +#define BATTERY_INITIALIZED 0x80 +#define BATTERY_DISCHARGING 0x40 +#define BATTERY_FULL_CHARGED 0x20 +#define BATTERY_FULL_DISCHARGED 0x10 + +/* min_value and max_value are only valid for numerical data */ +#define SBS_DATA(_psp, _addr, _min_value, _max_value) { \ + .psp = _psp, \ + .addr = _addr, \ + .min_value = _min_value, \ + .max_value = _max_value, \ +} + +static const struct chip_data { + enum power_supply_property psp; + u8 addr; + int min_value; + int max_value; +} sbs_data[] = { + [REG_MANUFACTURER_DATA] = + SBS_DATA(POWER_SUPPLY_PROP_PRESENT, 0x00, 0, 65535), + [REG_BATTERY_MODE] = + SBS_DATA(-1, 0x03, 0, 65535), + [REG_TEMPERATURE] = + SBS_DATA(POWER_SUPPLY_PROP_TEMP, 0x08, 0, 65535), + [REG_VOLTAGE] = + SBS_DATA(POWER_SUPPLY_PROP_VOLTAGE_NOW, 0x09, 0, 20000), + [REG_CURRENT_NOW] = + SBS_DATA(POWER_SUPPLY_PROP_CURRENT_NOW, 0x0A, -32768, 32767), + [REG_CURRENT_AVG] = + SBS_DATA(POWER_SUPPLY_PROP_CURRENT_AVG, 0x0B, -32768, 32767), + [REG_MAX_ERR] = + SBS_DATA(POWER_SUPPLY_PROP_CAPACITY_ERROR_MARGIN, 0x0c, 0, 100), + [REG_CAPACITY] = + SBS_DATA(POWER_SUPPLY_PROP_CAPACITY, 0x0D, 0, 100), + [REG_REMAINING_CAPACITY] = + SBS_DATA(POWER_SUPPLY_PROP_ENERGY_NOW, 0x0F, 0, 65535), + [REG_REMAINING_CAPACITY_CHARGE] = + SBS_DATA(POWER_SUPPLY_PROP_CHARGE_NOW, 0x0F, 0, 65535), + [REG_FULL_CHARGE_CAPACITY] = + SBS_DATA(POWER_SUPPLY_PROP_ENERGY_FULL, 0x10, 0, 65535), + [REG_FULL_CHARGE_CAPACITY_CHARGE] = + SBS_DATA(POWER_SUPPLY_PROP_CHARGE_FULL, 0x10, 0, 65535), + [REG_TIME_TO_EMPTY] = + SBS_DATA(POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, 0x12, 0, 65535), + [REG_TIME_TO_FULL] = + SBS_DATA(POWER_SUPPLY_PROP_TIME_TO_FULL_AVG, 0x13, 0, 65535), + [REG_CHARGE_CURRENT] = + SBS_DATA(POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, 0x14, 0, 65535), + [REG_CHARGE_VOLTAGE] = + SBS_DATA(POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, 0x15, 0, 65535), + [REG_STATUS] = + SBS_DATA(POWER_SUPPLY_PROP_STATUS, 0x16, 0, 65535), + [REG_CAPACITY_LEVEL] = + SBS_DATA(POWER_SUPPLY_PROP_CAPACITY_LEVEL, 0x16, 0, 65535), + [REG_CYCLE_COUNT] = + SBS_DATA(POWER_SUPPLY_PROP_CYCLE_COUNT, 0x17, 0, 65535), + [REG_DESIGN_CAPACITY] = + SBS_DATA(POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, 0x18, 0, 65535), + [REG_DESIGN_CAPACITY_CHARGE] = + SBS_DATA(POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, 0x18, 0, 65535), + [REG_DESIGN_VOLTAGE_MIN] = + SBS_DATA(POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, 0x19, 0, 65535), + [REG_DESIGN_VOLTAGE_MAX] = + SBS_DATA(POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, 0x19, 0, 65535), + [REG_SERIAL_NUMBER] = + SBS_DATA(POWER_SUPPLY_PROP_SERIAL_NUMBER, 0x1C, 0, 65535), + /* Properties of type `const char *' */ + [REG_MANUFACTURER] = + SBS_DATA(POWER_SUPPLY_PROP_MANUFACTURER, 0x20, 0, 65535), + [REG_MODEL_NAME] = + SBS_DATA(POWER_SUPPLY_PROP_MODEL_NAME, 0x21, 0, 65535), + [REG_CHEMISTRY] = + SBS_DATA(POWER_SUPPLY_PROP_TECHNOLOGY, 0x22, 0, 65535) +}; + +static const enum power_supply_property sbs_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_ERROR_MARGIN, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_AVG, + POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_ENERGY_FULL, + POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, + POWER_SUPPLY_PROP_MANUFACTURE_YEAR, + POWER_SUPPLY_PROP_MANUFACTURE_MONTH, + POWER_SUPPLY_PROP_MANUFACTURE_DAY, + /* Properties of type `const char *' */ + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME +}; + +/* Supports special manufacturer commands from TI BQ20Z65 and BQ20Z75 IC. */ +#define SBS_FLAGS_TI_BQ20ZX5 BIT(0) + +struct sbs_info { + struct i2c_client *client; + struct power_supply *power_supply; + bool is_present; + struct gpio_desc *gpio_detect; + bool charger_broadcasts; + int last_state; + int poll_time; + u32 i2c_retry_count; + u32 poll_retry_count; + struct delayed_work work; + struct mutex mode_lock; + u32 flags; +}; + +static char model_name[I2C_SMBUS_BLOCK_MAX + 1]; +static char manufacturer[I2C_SMBUS_BLOCK_MAX + 1]; +static char chemistry[I2C_SMBUS_BLOCK_MAX + 1]; +static bool force_load; + +static int sbs_read_word_data(struct i2c_client *client, u8 address); +static int sbs_write_word_data(struct i2c_client *client, u8 address, u16 value); + +static void sbs_disable_charger_broadcasts(struct sbs_info *chip) +{ + int val = sbs_read_word_data(chip->client, BATTERY_MODE_OFFSET); + if (val < 0) + goto exit; + + val |= BATTERY_MODE_CHARGER_MASK; + + val = sbs_write_word_data(chip->client, BATTERY_MODE_OFFSET, val); + +exit: + if (val < 0) + dev_err(&chip->client->dev, + "Failed to disable charger broadcasting: %d\n", val); + else + dev_dbg(&chip->client->dev, "%s\n", __func__); +} + +static int sbs_update_presence(struct sbs_info *chip, bool is_present) +{ + struct i2c_client *client = chip->client; + int retries = chip->i2c_retry_count; + s32 ret = 0; + u8 version; + + if (chip->is_present == is_present) + return 0; + + if (!is_present) { + chip->is_present = false; + /* Disable PEC when no device is present */ + client->flags &= ~I2C_CLIENT_PEC; + return 0; + } + + /* Check if device supports packet error checking and use it */ + while (retries > 0) { + ret = i2c_smbus_read_word_data(client, REG_ADDR_SPEC_INFO); + if (ret >= 0) + break; + + /* + * Some batteries trigger the detection pin before the + * I2C bus is properly connected. This works around the + * issue. + */ + msleep(100); + + retries--; + } + + if (ret < 0) { + dev_dbg(&client->dev, "failed to read spec info: %d\n", ret); + + /* fallback to old behaviour */ + client->flags &= ~I2C_CLIENT_PEC; + chip->is_present = true; + + return ret; + } + + version = (ret & SPEC_INFO_VERSION_MASK) >> SPEC_INFO_VERSION_SHIFT; + + if (version == SBS_VERSION_1_1_WITH_PEC) + client->flags |= I2C_CLIENT_PEC; + else + client->flags &= ~I2C_CLIENT_PEC; + + if (of_device_is_compatible(client->dev.parent->of_node, "google,cros-ec-i2c-tunnel") + && client->flags & I2C_CLIENT_PEC) { + dev_info(&client->dev, "Disabling PEC because of broken Cros-EC implementation\n"); + client->flags &= ~I2C_CLIENT_PEC; + } + + dev_dbg(&client->dev, "PEC: %s\n", (client->flags & I2C_CLIENT_PEC) ? + "enabled" : "disabled"); + + if (!chip->is_present && is_present && !chip->charger_broadcasts) + sbs_disable_charger_broadcasts(chip); + + chip->is_present = true; + + return 0; +} + +static int sbs_read_word_data(struct i2c_client *client, u8 address) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + int retries = chip->i2c_retry_count; + s32 ret = 0; + + while (retries > 0) { + ret = i2c_smbus_read_word_data(client, address); + if (ret >= 0) + break; + retries--; + } + + if (ret < 0) { + dev_dbg(&client->dev, + "%s: i2c read at address 0x%x failed\n", + __func__, address); + return ret; + } + + return ret; +} + +static int sbs_read_string_data_fallback(struct i2c_client *client, u8 address, char *values) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + s32 ret = 0, block_length = 0; + int retries_length, retries_block; + u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1]; + + retries_length = chip->i2c_retry_count; + retries_block = chip->i2c_retry_count; + + dev_warn_once(&client->dev, "I2C adapter does not support I2C_FUNC_SMBUS_READ_BLOCK_DATA.\n" + "Fallback method does not support PEC.\n"); + + /* Adapter needs to support these two functions */ + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_I2C_BLOCK)){ + return -ENODEV; + } + + /* Get the length of block data */ + while (retries_length > 0) { + ret = i2c_smbus_read_byte_data(client, address); + if (ret >= 0) + break; + retries_length--; + } + + if (ret < 0) { + dev_dbg(&client->dev, + "%s: i2c read at address 0x%x failed\n", + __func__, address); + return ret; + } + + /* block_length does not include NULL terminator */ + block_length = ret; + if (block_length > I2C_SMBUS_BLOCK_MAX) { + dev_err(&client->dev, + "%s: Returned block_length is longer than 0x%x\n", + __func__, I2C_SMBUS_BLOCK_MAX); + return -EINVAL; + } + + /* Get the block data */ + while (retries_block > 0) { + ret = i2c_smbus_read_i2c_block_data( + client, address, + block_length + 1, block_buffer); + if (ret >= 0) + break; + retries_block--; + } + + if (ret < 0) { + dev_dbg(&client->dev, + "%s: i2c read at address 0x%x failed\n", + __func__, address); + return ret; + } + + /* block_buffer[0] == block_length */ + memcpy(values, block_buffer + 1, block_length); + values[block_length] = '\0'; + + return ret; +} + +static int sbs_read_string_data(struct i2c_client *client, u8 address, char *values) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + int retries = chip->i2c_retry_count; + int ret = 0; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_READ_BLOCK_DATA)) { + bool pec = client->flags & I2C_CLIENT_PEC; + client->flags &= ~I2C_CLIENT_PEC; + ret = sbs_read_string_data_fallback(client, address, values); + if (pec) + client->flags |= I2C_CLIENT_PEC; + return ret; + } + + while (retries > 0) { + ret = i2c_smbus_read_block_data(client, address, values); + if (ret >= 0) + break; + retries--; + } + + if (ret < 0) { + dev_dbg(&client->dev, "failed to read block 0x%x: %d\n", address, ret); + return ret; + } + + /* add string termination */ + values[ret] = '\0'; + return ret; +} + +static int sbs_write_word_data(struct i2c_client *client, u8 address, + u16 value) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + int retries = chip->i2c_retry_count; + s32 ret = 0; + + while (retries > 0) { + ret = i2c_smbus_write_word_data(client, address, value); + if (ret >= 0) + break; + retries--; + } + + if (ret < 0) { + dev_dbg(&client->dev, + "%s: i2c write to address 0x%x failed\n", + __func__, address); + return ret; + } + + return 0; +} + +static int sbs_status_correct(struct i2c_client *client, int *intval) +{ + int ret; + + ret = sbs_read_word_data(client, sbs_data[REG_CURRENT_NOW].addr); + if (ret < 0) + return ret; + + ret = (s16)ret; + + /* Not drawing current -> not charging (i.e. idle) */ + if (*intval != POWER_SUPPLY_STATUS_FULL && ret == 0) + *intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + + if (*intval == POWER_SUPPLY_STATUS_FULL) { + /* Drawing or providing current when full */ + if (ret > 0) + *intval = POWER_SUPPLY_STATUS_CHARGING; + else if (ret < 0) + *intval = POWER_SUPPLY_STATUS_DISCHARGING; + } + + return 0; +} + +static bool sbs_bat_needs_calibration(struct i2c_client *client) +{ + int ret; + + ret = sbs_read_word_data(client, sbs_data[REG_BATTERY_MODE].addr); + if (ret < 0) + return false; + + return !!(ret & BIT(7)); +} + +static int sbs_get_ti_battery_presence_and_health( + struct i2c_client *client, enum power_supply_property psp, + union power_supply_propval *val) +{ + s32 ret; + + /* + * Write to ManufacturerAccess with ManufacturerAccess command + * and then read the status. + */ + ret = sbs_write_word_data(client, sbs_data[REG_MANUFACTURER_DATA].addr, + MANUFACTURER_ACCESS_STATUS); + if (ret < 0) { + if (psp == POWER_SUPPLY_PROP_PRESENT) + val->intval = 0; /* battery removed */ + return ret; + } + + ret = sbs_read_word_data(client, sbs_data[REG_MANUFACTURER_DATA].addr); + if (ret < 0) { + if (psp == POWER_SUPPLY_PROP_PRESENT) + val->intval = 0; /* battery removed */ + return ret; + } + + if (ret < sbs_data[REG_MANUFACTURER_DATA].min_value || + ret > sbs_data[REG_MANUFACTURER_DATA].max_value) { + val->intval = 0; + return 0; + } + + /* Mask the upper nibble of 2nd byte and + * lower byte of response then + * shift the result by 8 to get status*/ + ret &= 0x0F00; + ret >>= 8; + if (psp == POWER_SUPPLY_PROP_PRESENT) { + if (ret == 0x0F) + /* battery removed */ + val->intval = 0; + else + val->intval = 1; + } else if (psp == POWER_SUPPLY_PROP_HEALTH) { + if (ret == 0x09) + val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else if (ret == 0x0B) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (ret == 0x0C) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (sbs_bat_needs_calibration(client)) + val->intval = POWER_SUPPLY_HEALTH_CALIBRATION_REQUIRED; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + } + + return 0; +} + +static int sbs_get_battery_presence_and_health( + struct i2c_client *client, enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + int ret; + + if (chip->flags & SBS_FLAGS_TI_BQ20ZX5) + return sbs_get_ti_battery_presence_and_health(client, psp, val); + + /* Dummy command; if it succeeds, battery is present. */ + ret = sbs_read_word_data(client, sbs_data[REG_STATUS].addr); + + if (ret < 0) { /* battery not present*/ + if (psp == POWER_SUPPLY_PROP_PRESENT) { + val->intval = 0; + return 0; + } + return ret; + } + + if (psp == POWER_SUPPLY_PROP_PRESENT) + val->intval = 1; /* battery present */ + else { /* POWER_SUPPLY_PROP_HEALTH */ + if (sbs_bat_needs_calibration(client)) { + val->intval = POWER_SUPPLY_HEALTH_CALIBRATION_REQUIRED; + } else { + /* SBS spec doesn't have a general health command. */ + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + } + } + + return 0; +} + +static int sbs_get_battery_property(struct i2c_client *client, + int reg_offset, enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + s32 ret; + + ret = sbs_read_word_data(client, sbs_data[reg_offset].addr); + if (ret < 0) + return ret; + + /* returned values are 16 bit */ + if (sbs_data[reg_offset].min_value < 0) + ret = (s16)ret; + + if (ret >= sbs_data[reg_offset].min_value && + ret <= sbs_data[reg_offset].max_value) { + val->intval = ret; + if (psp == POWER_SUPPLY_PROP_CAPACITY_LEVEL) { + if (!(ret & BATTERY_INITIALIZED)) + val->intval = + POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + else if (ret & BATTERY_FULL_CHARGED) + val->intval = + POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (ret & BATTERY_FULL_DISCHARGED) + val->intval = + POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else + val->intval = + POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + return 0; + } else if (psp != POWER_SUPPLY_PROP_STATUS) { + return 0; + } + + if (ret & BATTERY_FULL_CHARGED) + val->intval = POWER_SUPPLY_STATUS_FULL; + else if (ret & BATTERY_DISCHARGING) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else + val->intval = POWER_SUPPLY_STATUS_CHARGING; + + sbs_status_correct(client, &val->intval); + + if (chip->poll_time == 0) + chip->last_state = val->intval; + else if (chip->last_state != val->intval) { + cancel_delayed_work_sync(&chip->work); + power_supply_changed(chip->power_supply); + chip->poll_time = 0; + } + } else { + if (psp == POWER_SUPPLY_PROP_STATUS) + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + else if (psp == POWER_SUPPLY_PROP_CAPACITY) + /* sbs spec says that this can be >100 % + * even if max value is 100 % + */ + val->intval = min(ret, 100); + else + val->intval = 0; + } + + return 0; +} + +static int sbs_get_battery_string_property(struct i2c_client *client, + int reg_offset, enum power_supply_property psp, char *val) +{ + s32 ret; + + ret = sbs_read_string_data(client, sbs_data[reg_offset].addr, val); + + if (ret < 0) + return ret; + + return 0; +} + +static void sbs_unit_adjustment(struct i2c_client *client, + enum power_supply_property psp, union power_supply_propval *val) +{ +#define BASE_UNIT_CONVERSION 1000 +#define BATTERY_MODE_CAP_MULT_WATT (10 * BASE_UNIT_CONVERSION) +#define TIME_UNIT_CONVERSION 60 +#define TEMP_KELVIN_TO_CELSIUS 2731 + switch (psp) { + case POWER_SUPPLY_PROP_ENERGY_NOW: + case POWER_SUPPLY_PROP_ENERGY_FULL: + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + /* sbs provides energy in units of 10mWh. + * Convert to µWh + */ + val->intval *= BATTERY_MODE_CAP_MULT_WATT; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + case POWER_SUPPLY_PROP_CURRENT_NOW: + case POWER_SUPPLY_PROP_CURRENT_AVG: + case POWER_SUPPLY_PROP_CHARGE_NOW: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval *= BASE_UNIT_CONVERSION; + break; + + case POWER_SUPPLY_PROP_TEMP: + /* sbs provides battery temperature in 0.1K + * so convert it to 0.1°C + */ + val->intval -= TEMP_KELVIN_TO_CELSIUS; + break; + + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + case POWER_SUPPLY_PROP_TIME_TO_FULL_AVG: + /* sbs provides time to empty and time to full in minutes. + * Convert to seconds + */ + val->intval *= TIME_UNIT_CONVERSION; + break; + + default: + dev_dbg(&client->dev, + "%s: no need for unit conversion %d\n", __func__, psp); + } +} + +static enum sbs_capacity_mode sbs_set_capacity_mode(struct i2c_client *client, + enum sbs_capacity_mode mode) +{ + int ret, original_val; + + original_val = sbs_read_word_data(client, BATTERY_MODE_OFFSET); + if (original_val < 0) + return original_val; + + if ((original_val & BATTERY_MODE_CAPACITY_MASK) == mode) + return mode; + + if (mode == CAPACITY_MODE_AMPS) + ret = original_val & ~BATTERY_MODE_CAPACITY_MASK; + else + ret = original_val | BATTERY_MODE_CAPACITY_MASK; + + ret = sbs_write_word_data(client, BATTERY_MODE_OFFSET, ret); + if (ret < 0) + return ret; + + usleep_range(1000, 2000); + + return original_val & BATTERY_MODE_CAPACITY_MASK; +} + +static int sbs_get_battery_capacity(struct i2c_client *client, + int reg_offset, enum power_supply_property psp, + union power_supply_propval *val) +{ + s32 ret; + enum sbs_capacity_mode mode = CAPACITY_MODE_WATTS; + + if (power_supply_is_amp_property(psp)) + mode = CAPACITY_MODE_AMPS; + + mode = sbs_set_capacity_mode(client, mode); + if ((int)mode < 0) + return mode; + + ret = sbs_read_word_data(client, sbs_data[reg_offset].addr); + if (ret < 0) + return ret; + + val->intval = ret; + + ret = sbs_set_capacity_mode(client, mode); + if (ret < 0) + return ret; + + return 0; +} + +static char sbs_serial[5]; +static int sbs_get_battery_serial_number(struct i2c_client *client, + union power_supply_propval *val) +{ + int ret; + + ret = sbs_read_word_data(client, sbs_data[REG_SERIAL_NUMBER].addr); + if (ret < 0) + return ret; + + sprintf(sbs_serial, "%04x", ret); + val->strval = sbs_serial; + + return 0; +} + +static int sbs_get_property_index(struct i2c_client *client, + enum power_supply_property psp) +{ + int count; + for (count = 0; count < ARRAY_SIZE(sbs_data); count++) + if (psp == sbs_data[count].psp) + return count; + + dev_warn(&client->dev, + "%s: Invalid Property - %d\n", __func__, psp); + + return -EINVAL; +} + +static int sbs_get_chemistry(struct i2c_client *client, + union power_supply_propval *val) +{ + enum power_supply_property psp = POWER_SUPPLY_PROP_TECHNOLOGY; + int ret; + + ret = sbs_get_property_index(client, psp); + if (ret < 0) + return ret; + + ret = sbs_get_battery_string_property(client, ret, psp, + chemistry); + if (ret < 0) + return ret; + + if (!strncasecmp(chemistry, "LION", 4)) + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + else if (!strncasecmp(chemistry, "LiP", 3)) + val->intval = POWER_SUPPLY_TECHNOLOGY_LIPO; + else if (!strncasecmp(chemistry, "NiCd", 4)) + val->intval = POWER_SUPPLY_TECHNOLOGY_NiCd; + else if (!strncasecmp(chemistry, "NiMH", 4)) + val->intval = POWER_SUPPLY_TECHNOLOGY_NiMH; + else + val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + + if (val->intval == POWER_SUPPLY_TECHNOLOGY_UNKNOWN) + dev_warn(&client->dev, "Unknown chemistry: %s\n", chemistry); + + return 0; +} + +static int sbs_get_battery_manufacture_date(struct i2c_client *client, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret; + u16 day, month, year; + + ret = sbs_read_word_data(client, REG_ADDR_MANUFACTURE_DATE); + if (ret < 0) + return ret; + + day = ret & GENMASK(4, 0); + month = (ret & GENMASK(8, 5)) >> 5; + year = ((ret & GENMASK(15, 9)) >> 9) + 1980; + + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + val->intval = year; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + val->intval = month; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + val->intval = day; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int sbs_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct sbs_info *chip = power_supply_get_drvdata(psy); + struct i2c_client *client = chip->client; + + if (chip->gpio_detect) { + ret = gpiod_get_value_cansleep(chip->gpio_detect); + if (ret < 0) + return ret; + if (psp == POWER_SUPPLY_PROP_PRESENT) { + val->intval = ret; + sbs_update_presence(chip, ret); + return 0; + } + if (ret == 0) + return -ENODATA; + } + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + case POWER_SUPPLY_PROP_HEALTH: + ret = sbs_get_battery_presence_and_health(client, psp, val); + + /* this can only be true if no gpio is used */ + if (psp == POWER_SUPPLY_PROP_PRESENT) + return 0; + break; + + case POWER_SUPPLY_PROP_TECHNOLOGY: + ret = sbs_get_chemistry(client, val); + if (ret < 0) + break; + + goto done; /* don't trigger power_supply_changed()! */ + + case POWER_SUPPLY_PROP_ENERGY_NOW: + case POWER_SUPPLY_PROP_ENERGY_FULL: + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + case POWER_SUPPLY_PROP_CHARGE_NOW: + case POWER_SUPPLY_PROP_CHARGE_FULL: + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = sbs_get_property_index(client, psp); + if (ret < 0) + break; + + /* sbs_get_battery_capacity() will change the battery mode + * temporarily to read the requested attribute. Ensure we stay + * in the desired mode for the duration of the attribute read. + */ + mutex_lock(&chip->mode_lock); + ret = sbs_get_battery_capacity(client, ret, psp, val); + mutex_unlock(&chip->mode_lock); + break; + + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + ret = sbs_get_battery_serial_number(client, val); + break; + + case POWER_SUPPLY_PROP_STATUS: + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + case POWER_SUPPLY_PROP_CYCLE_COUNT: + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + case POWER_SUPPLY_PROP_CURRENT_NOW: + case POWER_SUPPLY_PROP_CURRENT_AVG: + case POWER_SUPPLY_PROP_TEMP: + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + case POWER_SUPPLY_PROP_TIME_TO_FULL_AVG: + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: + case POWER_SUPPLY_PROP_CAPACITY: + case POWER_SUPPLY_PROP_CAPACITY_ERROR_MARGIN: + ret = sbs_get_property_index(client, psp); + if (ret < 0) + break; + + ret = sbs_get_battery_property(client, ret, psp, val); + break; + + case POWER_SUPPLY_PROP_MODEL_NAME: + ret = sbs_get_property_index(client, psp); + if (ret < 0) + break; + + ret = sbs_get_battery_string_property(client, ret, psp, + model_name); + val->strval = model_name; + break; + + case POWER_SUPPLY_PROP_MANUFACTURER: + ret = sbs_get_property_index(client, psp); + if (ret < 0) + break; + + ret = sbs_get_battery_string_property(client, ret, psp, + manufacturer); + val->strval = manufacturer; + break; + + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + ret = sbs_get_battery_manufacture_date(client, psp, val); + break; + + default: + dev_err(&client->dev, + "%s: INVALID property\n", __func__); + return -EINVAL; + } + + if (!chip->gpio_detect && chip->is_present != (ret >= 0)) { + bool old_present = chip->is_present; + union power_supply_propval val; + int err = sbs_get_battery_presence_and_health( + client, POWER_SUPPLY_PROP_PRESENT, &val); + + sbs_update_presence(chip, !err && val.intval); + + if (old_present != chip->is_present) + power_supply_changed(chip->power_supply); + } + +done: + if (!ret) { + /* Convert units to match requirements for power supply class */ + sbs_unit_adjustment(client, psp, val); + dev_dbg(&client->dev, + "%s: property = %d, value = %x\n", __func__, + psp, val->intval); + } else if (!chip->is_present) { + /* battery not present, so return NODATA for properties */ + ret = -ENODATA; + } + return ret; +} + +static void sbs_supply_changed(struct sbs_info *chip) +{ + struct power_supply *battery = chip->power_supply; + int ret; + + ret = gpiod_get_value_cansleep(chip->gpio_detect); + if (ret < 0) + return; + sbs_update_presence(chip, ret); + power_supply_changed(battery); +} + +static irqreturn_t sbs_irq(int irq, void *devid) +{ + sbs_supply_changed(devid); + return IRQ_HANDLED; +} + +static void sbs_alert(struct i2c_client *client, enum i2c_alert_protocol prot, + unsigned int data) +{ + sbs_supply_changed(i2c_get_clientdata(client)); +} + +static void sbs_external_power_changed(struct power_supply *psy) +{ + struct sbs_info *chip = power_supply_get_drvdata(psy); + + /* cancel outstanding work */ + cancel_delayed_work_sync(&chip->work); + + schedule_delayed_work(&chip->work, HZ); + chip->poll_time = chip->poll_retry_count; +} + +static void sbs_delayed_work(struct work_struct *work) +{ + struct sbs_info *chip; + s32 ret; + + chip = container_of(work, struct sbs_info, work.work); + + ret = sbs_read_word_data(chip->client, sbs_data[REG_STATUS].addr); + /* if the read failed, give up on this work */ + if (ret < 0) { + chip->poll_time = 0; + return; + } + + if (ret & BATTERY_FULL_CHARGED) + ret = POWER_SUPPLY_STATUS_FULL; + else if (ret & BATTERY_DISCHARGING) + ret = POWER_SUPPLY_STATUS_DISCHARGING; + else + ret = POWER_SUPPLY_STATUS_CHARGING; + + sbs_status_correct(chip->client, &ret); + + if (chip->last_state != ret) { + chip->poll_time = 0; + power_supply_changed(chip->power_supply); + return; + } + if (chip->poll_time > 0) { + schedule_delayed_work(&chip->work, HZ); + chip->poll_time--; + return; + } +} + +static const struct power_supply_desc sbs_default_desc = { + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = sbs_properties, + .num_properties = ARRAY_SIZE(sbs_properties), + .get_property = sbs_get_property, + .external_power_changed = sbs_external_power_changed, +}; + +static int sbs_probe(struct i2c_client *client) +{ + struct sbs_info *chip; + struct power_supply_desc *sbs_desc; + struct sbs_platform_data *pdata = client->dev.platform_data; + struct power_supply_config psy_cfg = {}; + int rc; + int irq; + + sbs_desc = devm_kmemdup(&client->dev, &sbs_default_desc, + sizeof(*sbs_desc), GFP_KERNEL); + if (!sbs_desc) + return -ENOMEM; + + sbs_desc->name = devm_kasprintf(&client->dev, GFP_KERNEL, "sbs-%s", + dev_name(&client->dev)); + if (!sbs_desc->name) + return -ENOMEM; + + chip = devm_kzalloc(&client->dev, sizeof(struct sbs_info), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->flags = (u32)(uintptr_t)device_get_match_data(&client->dev); + chip->client = client; + psy_cfg.of_node = client->dev.of_node; + psy_cfg.drv_data = chip; + chip->last_state = POWER_SUPPLY_STATUS_UNKNOWN; + mutex_init(&chip->mode_lock); + + /* use pdata if available, fall back to DT properties, + * or hardcoded defaults if not + */ + rc = device_property_read_u32(&client->dev, "sbs,i2c-retry-count", + &chip->i2c_retry_count); + if (rc) + chip->i2c_retry_count = 0; + + rc = device_property_read_u32(&client->dev, "sbs,poll-retry-count", + &chip->poll_retry_count); + if (rc) + chip->poll_retry_count = 0; + + if (pdata) { + chip->poll_retry_count = pdata->poll_retry_count; + chip->i2c_retry_count = pdata->i2c_retry_count; + } + chip->i2c_retry_count = chip->i2c_retry_count + 1; + + chip->charger_broadcasts = !device_property_read_bool(&client->dev, + "sbs,disable-charger-broadcasts"); + + chip->gpio_detect = devm_gpiod_get_optional(&client->dev, + "sbs,battery-detect", GPIOD_IN); + if (IS_ERR(chip->gpio_detect)) { + dev_err(&client->dev, "Failed to get gpio: %ld\n", + PTR_ERR(chip->gpio_detect)); + return PTR_ERR(chip->gpio_detect); + } + + i2c_set_clientdata(client, chip); + + if (!chip->gpio_detect) + goto skip_gpio; + + irq = gpiod_to_irq(chip->gpio_detect); + if (irq <= 0) { + dev_warn(&client->dev, "Failed to get gpio as irq: %d\n", irq); + goto skip_gpio; + } + + rc = devm_request_threaded_irq(&client->dev, irq, NULL, sbs_irq, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + dev_name(&client->dev), chip); + if (rc) { + dev_warn(&client->dev, "Failed to request irq: %d\n", rc); + goto skip_gpio; + } + +skip_gpio: + /* + * Before we register, we might need to make sure we can actually talk + * to the battery. + */ + if (!(force_load || chip->gpio_detect)) { + union power_supply_propval val; + + rc = sbs_get_battery_presence_and_health( + client, POWER_SUPPLY_PROP_PRESENT, &val); + if (rc < 0 || !val.intval) { + dev_err(&client->dev, "Failed to get present status\n"); + rc = -ENODEV; + goto exit_psupply; + } + } + + INIT_DELAYED_WORK(&chip->work, sbs_delayed_work); + + chip->power_supply = devm_power_supply_register(&client->dev, sbs_desc, + &psy_cfg); + if (IS_ERR(chip->power_supply)) { + dev_err(&client->dev, + "%s: Failed to register power supply\n", __func__); + rc = PTR_ERR(chip->power_supply); + goto exit_psupply; + } + + dev_info(&client->dev, + "%s: battery gas gauge device registered\n", client->name); + + return 0; + +exit_psupply: + return rc; +} + +static int sbs_remove(struct i2c_client *client) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&chip->work); + + return 0; +} + +#if defined CONFIG_PM_SLEEP + +static int sbs_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct sbs_info *chip = i2c_get_clientdata(client); + int ret; + + if (chip->poll_time > 0) + cancel_delayed_work_sync(&chip->work); + + if (chip->flags & SBS_FLAGS_TI_BQ20ZX5) { + /* Write to manufacturer access with sleep command. */ + ret = sbs_write_word_data(client, + sbs_data[REG_MANUFACTURER_DATA].addr, + MANUFACTURER_ACCESS_SLEEP); + if (chip->is_present && ret < 0) + return ret; + } + + return 0; +} + +static SIMPLE_DEV_PM_OPS(sbs_pm_ops, sbs_suspend, NULL); +#define SBS_PM_OPS (&sbs_pm_ops) + +#else +#define SBS_PM_OPS NULL +#endif + +static const struct i2c_device_id sbs_id[] = { + { "bq20z65", 0 }, + { "bq20z75", 0 }, + { "sbs-battery", 1 }, + {} +}; +MODULE_DEVICE_TABLE(i2c, sbs_id); + +static const struct of_device_id sbs_dt_ids[] = { + { .compatible = "sbs,sbs-battery" }, + { + .compatible = "ti,bq20z65", + .data = (void *)SBS_FLAGS_TI_BQ20ZX5, + }, + { + .compatible = "ti,bq20z75", + .data = (void *)SBS_FLAGS_TI_BQ20ZX5, + }, + { } +}; +MODULE_DEVICE_TABLE(of, sbs_dt_ids); + +static struct i2c_driver sbs_battery_driver = { + .probe_new = sbs_probe, + .remove = sbs_remove, + .alert = sbs_alert, + .id_table = sbs_id, + .driver = { + .name = "sbs-battery", + .of_match_table = sbs_dt_ids, + .pm = SBS_PM_OPS, + }, +}; +module_i2c_driver(sbs_battery_driver); + +MODULE_DESCRIPTION("SBS battery monitor driver"); +MODULE_LICENSE("GPL"); + +module_param(force_load, bool, 0444); +MODULE_PARM_DESC(force_load, + "Attempt to load the driver even if no battery is connected"); diff --git a/drivers/power/supply/sbs-charger.c b/drivers/power/supply/sbs-charger.c new file mode 100644 index 000000000..a30eb42e3 --- /dev/null +++ b/drivers/power/supply/sbs-charger.c @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2016, Prodys S.L. + * + * This adds support for sbs-charger compilant chips as defined here: + * http://sbs-forum.org/specs/sbc110.pdf + * + * Implemetation based on sbs-battery.c + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/err.h> +#include <linux/power_supply.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/gpio.h> +#include <linux/regmap.h> +#include <linux/of_gpio.h> +#include <linux/bitops.h> + +#define SBS_CHARGER_REG_SPEC_INFO 0x11 +#define SBS_CHARGER_REG_STATUS 0x13 +#define SBS_CHARGER_REG_ALARM_WARNING 0x16 + +#define SBS_CHARGER_STATUS_CHARGE_INHIBITED BIT(0) +#define SBS_CHARGER_STATUS_RES_COLD BIT(9) +#define SBS_CHARGER_STATUS_RES_HOT BIT(10) +#define SBS_CHARGER_STATUS_BATTERY_PRESENT BIT(14) +#define SBS_CHARGER_STATUS_AC_PRESENT BIT(15) + +#define SBS_CHARGER_POLL_TIME 500 + +struct sbs_info { + struct i2c_client *client; + struct power_supply *power_supply; + struct regmap *regmap; + struct delayed_work work; + unsigned int last_state; +}; + +static int sbs_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sbs_info *chip = power_supply_get_drvdata(psy); + unsigned int reg; + + reg = chip->last_state; + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + val->intval = !!(reg & SBS_CHARGER_STATUS_BATTERY_PRESENT); + break; + + case POWER_SUPPLY_PROP_ONLINE: + val->intval = !!(reg & SBS_CHARGER_STATUS_AC_PRESENT); + break; + + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + + if (!(reg & SBS_CHARGER_STATUS_BATTERY_PRESENT)) + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + else if (reg & SBS_CHARGER_STATUS_AC_PRESENT && + !(reg & SBS_CHARGER_STATUS_CHARGE_INHIBITED)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + + break; + + case POWER_SUPPLY_PROP_HEALTH: + if (reg & SBS_CHARGER_STATUS_RES_COLD) + val->intval = POWER_SUPPLY_HEALTH_COLD; + if (reg & SBS_CHARGER_STATUS_RES_HOT) + val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int sbs_check_state(struct sbs_info *chip) +{ + unsigned int reg; + int ret; + + ret = regmap_read(chip->regmap, SBS_CHARGER_REG_STATUS, ®); + if (!ret && reg != chip->last_state) { + chip->last_state = reg; + power_supply_changed(chip->power_supply); + return 1; + } + + return 0; +} + +static void sbs_delayed_work(struct work_struct *work) +{ + struct sbs_info *chip = container_of(work, struct sbs_info, work.work); + + sbs_check_state(chip); + + schedule_delayed_work(&chip->work, + msecs_to_jiffies(SBS_CHARGER_POLL_TIME)); +} + +static irqreturn_t sbs_irq_thread(int irq, void *data) +{ + struct sbs_info *chip = data; + int ret; + + ret = sbs_check_state(chip); + + return ret ? IRQ_HANDLED : IRQ_NONE; +} + +static enum power_supply_property sbs_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_HEALTH, +}; + +static bool sbs_readable_reg(struct device *dev, unsigned int reg) +{ + return reg >= SBS_CHARGER_REG_SPEC_INFO; +} + +static bool sbs_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case SBS_CHARGER_REG_STATUS: + return true; + } + + return false; +} + +static const struct regmap_config sbs_regmap = { + .reg_bits = 8, + .val_bits = 16, + .max_register = SBS_CHARGER_REG_ALARM_WARNING, + .readable_reg = sbs_readable_reg, + .volatile_reg = sbs_volatile_reg, + .val_format_endian = REGMAP_ENDIAN_LITTLE, /* since based on SMBus */ +}; + +static const struct power_supply_desc sbs_desc = { + .name = "sbs-charger", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = sbs_properties, + .num_properties = ARRAY_SIZE(sbs_properties), + .get_property = sbs_get_property, +}; + +static int sbs_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct power_supply_config psy_cfg = {}; + struct sbs_info *chip; + int ret, val; + + chip = devm_kzalloc(&client->dev, sizeof(struct sbs_info), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->client = client; + psy_cfg.of_node = client->dev.of_node; + psy_cfg.drv_data = chip; + + i2c_set_clientdata(client, chip); + + chip->regmap = devm_regmap_init_i2c(client, &sbs_regmap); + if (IS_ERR(chip->regmap)) + return PTR_ERR(chip->regmap); + + /* + * Before we register, we need to make sure we can actually talk + * to the battery. + */ + ret = regmap_read(chip->regmap, SBS_CHARGER_REG_STATUS, &val); + if (ret) { + dev_err(&client->dev, "Failed to get device status\n"); + return ret; + } + chip->last_state = val; + + chip->power_supply = devm_power_supply_register(&client->dev, &sbs_desc, + &psy_cfg); + if (IS_ERR(chip->power_supply)) { + dev_err(&client->dev, "Failed to register power supply\n"); + return PTR_ERR(chip->power_supply); + } + + /* + * The sbs-charger spec doesn't impose the use of an interrupt. So in + * the case it wasn't provided we use polling in order get the charger's + * status. + */ + if (client->irq) { + ret = devm_request_threaded_irq(&client->dev, client->irq, + NULL, sbs_irq_thread, + IRQF_TRIGGER_FALLING | IRQF_ONESHOT, + dev_name(&client->dev), chip); + if (ret) { + dev_err(&client->dev, "Failed to request irq, %d\n", ret); + return ret; + } + } else { + INIT_DELAYED_WORK(&chip->work, sbs_delayed_work); + schedule_delayed_work(&chip->work, + msecs_to_jiffies(SBS_CHARGER_POLL_TIME)); + } + + dev_info(&client->dev, + "%s: smart charger device registered\n", client->name); + + return 0; +} + +static int sbs_remove(struct i2c_client *client) +{ + struct sbs_info *chip = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&chip->work); + + return 0; +} + +#ifdef CONFIG_OF +static const struct of_device_id sbs_dt_ids[] = { + { .compatible = "sbs,sbs-charger" }, + { }, +}; +MODULE_DEVICE_TABLE(of, sbs_dt_ids); +#endif + +static const struct i2c_device_id sbs_id[] = { + { "sbs-charger", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, sbs_id); + +static struct i2c_driver sbs_driver = { + .probe = sbs_probe, + .remove = sbs_remove, + .id_table = sbs_id, + .driver = { + .name = "sbs-charger", + .of_match_table = of_match_ptr(sbs_dt_ids), + }, +}; +module_i2c_driver(sbs_driver); + +MODULE_AUTHOR("Nicolas Saenz Julienne <nicolassaenzj@gmail.com>"); +MODULE_DESCRIPTION("SBS smart charger driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/sbs-manager.c b/drivers/power/supply/sbs-manager.c new file mode 100644 index 000000000..666243d9d --- /dev/null +++ b/drivers/power/supply/sbs-manager.c @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for SBS compliant Smart Battery System Managers + * + * The device communicates via i2c at address 0x0a and multiplexes access to up + * to four smart batteries at address 0x0b. + * + * Via sysfs interface the online state and charge type are presented. + * + * Datasheet SBSM: http://sbs-forum.org/specs/sbsm100b.pdf + * Datasheet LTC1760: http://cds.linear.com/docs/en/datasheet/1760fb.pdf + * + * Karl-Heinz Schneider <karl-heinz@schneider-inet.de> + */ + +#include <linux/gpio.h> +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/i2c-mux.h> +#include <linux/power_supply.h> +#include <linux/property.h> + +#define SBSM_MAX_BATS 4 +#define SBSM_RETRY_CNT 3 + +/* registers addresses */ +#define SBSM_CMD_BATSYSSTATE 0x01 +#define SBSM_CMD_BATSYSSTATECONT 0x02 +#define SBSM_CMD_BATSYSINFO 0x04 +#define SBSM_CMD_LTC 0x3c + +#define SBSM_MASK_BAT_SUPPORTED GENMASK(3, 0) +#define SBSM_MASK_CHARGE_BAT GENMASK(7, 4) +#define SBSM_BIT_AC_PRESENT BIT(0) +#define SBSM_BIT_TURBO BIT(7) + +#define SBSM_SMB_BAT_OFFSET 11 +struct sbsm_data { + struct i2c_client *client; + struct i2c_mux_core *muxc; + + struct power_supply *psy; + + u8 cur_chan; /* currently selected channel */ + struct gpio_chip chip; + bool is_ltc1760; /* special capabilities */ + + unsigned int supported_bats; + unsigned int last_state; + unsigned int last_state_cont; +}; + +static enum power_supply_property sbsm_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CHARGE_TYPE, +}; + +static int sbsm_read_word(struct i2c_client *client, u8 address) +{ + int reg, retries; + + for (retries = SBSM_RETRY_CNT; retries > 0; retries--) { + reg = i2c_smbus_read_word_data(client, address); + if (reg >= 0) + break; + } + + if (reg < 0) { + dev_err(&client->dev, "failed to read register 0x%02x\n", + address); + } + + return reg; +} + +static int sbsm_write_word(struct i2c_client *client, u8 address, u16 word) +{ + int ret, retries; + + for (retries = SBSM_RETRY_CNT; retries > 0; retries--) { + ret = i2c_smbus_write_word_data(client, address, word); + if (ret >= 0) + break; + } + if (ret < 0) + dev_err(&client->dev, "failed to write to register 0x%02x\n", + address); + + return ret; +} + +static int sbsm_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sbsm_data *data = power_supply_get_drvdata(psy); + int regval = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + regval = sbsm_read_word(data->client, SBSM_CMD_BATSYSSTATECONT); + if (regval < 0) + return regval; + val->intval = !!(regval & SBSM_BIT_AC_PRESENT); + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + regval = sbsm_read_word(data->client, SBSM_CMD_BATSYSSTATE); + if (regval < 0) + return regval; + + if ((regval & SBSM_MASK_CHARGE_BAT) == 0) { + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + return 0; + } + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + + if (data->is_ltc1760) { + /* charge mode fast if turbo is active */ + regval = sbsm_read_word(data->client, SBSM_CMD_LTC); + if (regval < 0) + return regval; + else if (regval & SBSM_BIT_TURBO) + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + } + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int sbsm_prop_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + struct sbsm_data *data = power_supply_get_drvdata(psy); + + return (psp == POWER_SUPPLY_PROP_CHARGE_TYPE) && data->is_ltc1760; +} + +static int sbsm_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct sbsm_data *data = power_supply_get_drvdata(psy); + int ret = -EINVAL; + u16 regval; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPE: + /* write 1 to TURBO if type fast is given */ + if (!data->is_ltc1760) + break; + regval = val->intval == + POWER_SUPPLY_CHARGE_TYPE_FAST ? SBSM_BIT_TURBO : 0; + ret = sbsm_write_word(data->client, SBSM_CMD_LTC, regval); + break; + + default: + break; + } + + return ret; +} + +/* + * Switch to battery + * Parameter chan is directly the content of SMB_BAT* nibble + */ +static int sbsm_select(struct i2c_mux_core *muxc, u32 chan) +{ + struct sbsm_data *data = i2c_mux_priv(muxc); + struct device *dev = &data->client->dev; + int ret = 0; + u16 reg; + + if (data->cur_chan == chan) + return ret; + + /* chan goes from 1 ... 4 */ + reg = BIT(SBSM_SMB_BAT_OFFSET + chan); + ret = sbsm_write_word(data->client, SBSM_CMD_BATSYSSTATE, reg); + if (ret) + dev_err(dev, "Failed to select channel %i\n", chan); + else + data->cur_chan = chan; + + return ret; +} + +static int sbsm_gpio_get_value(struct gpio_chip *gc, unsigned int off) +{ + struct sbsm_data *data = gpiochip_get_data(gc); + int ret; + + ret = sbsm_read_word(data->client, SBSM_CMD_BATSYSSTATE); + if (ret < 0) + return ret; + + return ret & BIT(off); +} + +/* + * This needs to be defined or the GPIO lib fails to register the pin. + * But the 'gpio' is always an input. + */ +static int sbsm_gpio_direction_input(struct gpio_chip *gc, unsigned int off) +{ + return 0; +} + +static int sbsm_do_alert(struct device *dev, void *d) +{ + struct i2c_client *client = i2c_verify_client(dev); + struct i2c_driver *driver; + + if (!client || client->addr != 0x0b) + return 0; + + device_lock(dev); + if (client->dev.driver) { + driver = to_i2c_driver(client->dev.driver); + if (driver->alert) + driver->alert(client, I2C_PROTOCOL_SMBUS_ALERT, 0); + else + dev_warn(&client->dev, "no driver alert()!\n"); + } else { + dev_dbg(&client->dev, "alert with no driver\n"); + } + device_unlock(dev); + + return -EBUSY; +} + +static void sbsm_alert(struct i2c_client *client, enum i2c_alert_protocol prot, + unsigned int d) +{ + struct sbsm_data *sbsm = i2c_get_clientdata(client); + + int ret, i, irq_bat = 0, state = 0; + + ret = sbsm_read_word(sbsm->client, SBSM_CMD_BATSYSSTATE); + if (ret >= 0) { + irq_bat = ret ^ sbsm->last_state; + sbsm->last_state = ret; + state = ret; + } + + ret = sbsm_read_word(sbsm->client, SBSM_CMD_BATSYSSTATECONT); + if ((ret >= 0) && + ((ret ^ sbsm->last_state_cont) & SBSM_BIT_AC_PRESENT)) { + irq_bat |= sbsm->supported_bats & state; + power_supply_changed(sbsm->psy); + } + sbsm->last_state_cont = ret; + + for (i = 0; i < SBSM_MAX_BATS; i++) { + if (irq_bat & BIT(i)) { + device_for_each_child(&sbsm->muxc->adapter[i]->dev, + NULL, sbsm_do_alert); + } + } +} + +static int sbsm_gpio_setup(struct sbsm_data *data) +{ + struct gpio_chip *gc = &data->chip; + struct i2c_client *client = data->client; + struct device *dev = &client->dev; + int ret; + + if (!device_property_present(dev, "gpio-controller")) + return 0; + + ret = sbsm_read_word(client, SBSM_CMD_BATSYSSTATE); + if (ret < 0) + return ret; + data->last_state = ret; + + ret = sbsm_read_word(client, SBSM_CMD_BATSYSSTATECONT); + if (ret < 0) + return ret; + data->last_state_cont = ret; + + gc->get = sbsm_gpio_get_value; + gc->direction_input = sbsm_gpio_direction_input; + gc->can_sleep = true; + gc->base = -1; + gc->ngpio = SBSM_MAX_BATS; + gc->label = client->name; + gc->parent = dev; + gc->owner = THIS_MODULE; + + ret = devm_gpiochip_add_data(dev, gc, data); + if (ret) { + dev_err(dev, "devm_gpiochip_add_data failed: %d\n", ret); + return ret; + } + + return ret; +} + +static const struct power_supply_desc sbsm_default_psy_desc = { + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = sbsm_props, + .num_properties = ARRAY_SIZE(sbsm_props), + .get_property = &sbsm_get_property, + .set_property = &sbsm_set_property, + .property_is_writeable = &sbsm_prop_is_writeable, +}; + +static int sbsm_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct i2c_adapter *adapter = client->adapter; + struct sbsm_data *data; + struct device *dev = &client->dev; + struct power_supply_desc *psy_desc; + struct power_supply_config psy_cfg = {}; + int ret = 0, i; + + /* Device listens only at address 0x0a */ + if (client->addr != 0x0a) + return -EINVAL; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_WORD_DATA)) + return -EPFNOSUPPORT; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + i2c_set_clientdata(client, data); + + data->client = client; + data->is_ltc1760 = !!strstr(id->name, "ltc1760"); + + ret = sbsm_read_word(client, SBSM_CMD_BATSYSINFO); + if (ret < 0) + return ret; + data->supported_bats = ret & SBSM_MASK_BAT_SUPPORTED; + data->muxc = i2c_mux_alloc(adapter, dev, SBSM_MAX_BATS, 0, + I2C_MUX_LOCKED, &sbsm_select, NULL); + if (!data->muxc) { + dev_err(dev, "failed to alloc i2c mux\n"); + ret = -ENOMEM; + goto err_mux_alloc; + } + data->muxc->priv = data; + + /* register muxed i2c channels. One for each supported battery */ + for (i = 0; i < SBSM_MAX_BATS; ++i) { + if (data->supported_bats & BIT(i)) { + ret = i2c_mux_add_adapter(data->muxc, 0, i + 1, 0); + if (ret) + break; + } + } + if (ret) { + dev_err(dev, "failed to register i2c mux channel %d\n", i + 1); + goto err_mux_register; + } + + psy_desc = devm_kmemdup(dev, &sbsm_default_psy_desc, + sizeof(struct power_supply_desc), + GFP_KERNEL); + if (!psy_desc) { + ret = -ENOMEM; + goto err_psy; + } + + psy_desc->name = devm_kasprintf(dev, GFP_KERNEL, "sbsm-%s", + dev_name(&client->dev)); + if (!psy_desc->name) { + ret = -ENOMEM; + goto err_psy; + } + ret = sbsm_gpio_setup(data); + if (ret < 0) + goto err_psy; + + psy_cfg.drv_data = data; + psy_cfg.of_node = dev->of_node; + data->psy = devm_power_supply_register(dev, psy_desc, &psy_cfg); + if (IS_ERR(data->psy)) { + ret = PTR_ERR(data->psy); + dev_err(dev, "failed to register power supply %s\n", + psy_desc->name); + goto err_psy; + } + + return 0; + +err_psy: +err_mux_register: + i2c_mux_del_adapters(data->muxc); + +err_mux_alloc: + return ret; +} + +static int sbsm_remove(struct i2c_client *client) +{ + struct sbsm_data *data = i2c_get_clientdata(client); + + i2c_mux_del_adapters(data->muxc); + return 0; +} + +static const struct i2c_device_id sbsm_ids[] = { + { "sbs-manager", 0 }, + { "ltc1760", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, sbsm_ids); + +#ifdef CONFIG_OF +static const struct of_device_id sbsm_dt_ids[] = { + { .compatible = "sbs,sbs-manager" }, + { .compatible = "lltc,ltc1760" }, + { } +}; +MODULE_DEVICE_TABLE(of, sbsm_dt_ids); +#endif + +static struct i2c_driver sbsm_driver = { + .driver = { + .name = "sbsm", + .of_match_table = of_match_ptr(sbsm_dt_ids), + }, + .probe = sbsm_probe, + .remove = sbsm_remove, + .alert = sbsm_alert, + .id_table = sbsm_ids +}; +module_i2c_driver(sbsm_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Karl-Heinz Schneider <karl-heinz@schneider-inet.de>"); +MODULE_DESCRIPTION("SBSM Smart Battery System Manager"); diff --git a/drivers/power/supply/sc2731_charger.c b/drivers/power/supply/sc2731_charger.c new file mode 100644 index 000000000..288b79836 --- /dev/null +++ b/drivers/power/supply/sc2731_charger.c @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2018 Spreadtrum Communications Inc. + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/usb/phy.h> +#include <linux/regmap.h> +#include <linux/notifier.h> +#include <linux/of.h> + +/* PMIC global registers definition */ +#define SC2731_CHARGE_STATUS 0xedc +#define SC2731_CHARGE_FULL BIT(4) +#define SC2731_MODULE_EN1 0xc0c +#define SC2731_CHARGE_EN BIT(5) + +/* SC2731 switch charger registers definition */ +#define SC2731_CHG_CFG0 0x0 +#define SC2731_CHG_CFG1 0x4 +#define SC2731_CHG_CFG2 0x8 +#define SC2731_CHG_CFG3 0xc +#define SC2731_CHG_CFG4 0x10 +#define SC2731_CHG_CFG5 0x28 + +/* SC2731_CHG_CFG0 register definition */ +#define SC2731_PRECHG_RNG_SHIFT 11 +#define SC2731_PRECHG_RNG_MASK GENMASK(12, 11) + +#define SC2731_TERMINATION_VOL_MASK GENMASK(2, 1) +#define SC2731_TERMINATION_VOL_SHIFT 1 +#define SC2731_TERMINATION_VOL_CAL_MASK GENMASK(8, 3) +#define SC2731_TERMINATION_VOL_CAL_SHIFT 3 +#define SC2731_TERMINATION_CUR_MASK GENMASK(2, 0) + +#define SC2731_CC_EN BIT(13) +#define SC2731_CHARGER_PD BIT(0) + +/* SC2731_CHG_CFG1 register definition */ +#define SC2731_CUR_MASK GENMASK(5, 0) + +/* SC2731_CHG_CFG5 register definition */ +#define SC2731_CUR_LIMIT_SHIFT 8 +#define SC2731_CUR_LIMIT_MASK GENMASK(9, 8) + +/* Default current definition (unit is mA) */ +#define SC2731_CURRENT_LIMIT_100 100 +#define SC2731_CURRENT_LIMIT_500 500 +#define SC2731_CURRENT_LIMIT_900 900 +#define SC2731_CURRENT_LIMIT_2000 2000 +#define SC2731_CURRENT_PRECHG 450 +#define SC2731_CURRENT_STEP 50 + +struct sc2731_charger_info { + struct device *dev; + struct regmap *regmap; + struct usb_phy *usb_phy; + struct notifier_block usb_notify; + struct power_supply *psy_usb; + struct work_struct work; + struct mutex lock; + bool charging; + u32 base; + u32 limit; +}; + +static void sc2731_charger_stop_charge(struct sc2731_charger_info *info) +{ + regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_CC_EN, 0); + + regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_CHARGER_PD, SC2731_CHARGER_PD); +} + +static int sc2731_charger_start_charge(struct sc2731_charger_info *info) +{ + int ret; + + /* Enable charger constant current mode */ + ret = regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_CC_EN, SC2731_CC_EN); + if (ret) + return ret; + + /* Start charging */ + return regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_CHARGER_PD, 0); +} + +static int sc2731_charger_set_current_limit(struct sc2731_charger_info *info, + u32 limit) +{ + u32 val; + + if (limit <= SC2731_CURRENT_LIMIT_100) + val = 0; + else if (limit <= SC2731_CURRENT_LIMIT_500) + val = 3; + else if (limit <= SC2731_CURRENT_LIMIT_900) + val = 2; + else + val = 1; + + return regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG5, + SC2731_CUR_LIMIT_MASK, + val << SC2731_CUR_LIMIT_SHIFT); +} + +static int sc2731_charger_set_current(struct sc2731_charger_info *info, u32 cur) +{ + u32 val; + int ret; + + if (cur > SC2731_CURRENT_LIMIT_2000) + cur = SC2731_CURRENT_LIMIT_2000; + else if (cur < SC2731_CURRENT_PRECHG) + cur = SC2731_CURRENT_PRECHG; + + /* Calculate the step value, each step is 50 mA */ + val = (cur - SC2731_CURRENT_PRECHG) / SC2731_CURRENT_STEP; + + /* Set pre-charge current as 450 mA */ + ret = regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_PRECHG_RNG_MASK, + 0x3 << SC2731_PRECHG_RNG_SHIFT); + if (ret) + return ret; + + return regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG1, + SC2731_CUR_MASK, val); +} + +static int sc2731_charger_get_status(struct sc2731_charger_info *info) +{ + u32 val; + int ret; + + ret = regmap_read(info->regmap, SC2731_CHARGE_STATUS, &val); + if (ret) + return ret; + + if (val & SC2731_CHARGE_FULL) + return POWER_SUPPLY_STATUS_FULL; + + return POWER_SUPPLY_STATUS_CHARGING; +} + +static int sc2731_charger_get_current(struct sc2731_charger_info *info, + u32 *cur) +{ + int ret; + u32 val; + + ret = regmap_read(info->regmap, info->base + SC2731_CHG_CFG1, &val); + if (ret) + return ret; + + val &= SC2731_CUR_MASK; + *cur = val * SC2731_CURRENT_STEP + SC2731_CURRENT_PRECHG; + + return 0; +} + +static int sc2731_charger_get_current_limit(struct sc2731_charger_info *info, + u32 *cur) +{ + int ret; + u32 val; + + ret = regmap_read(info->regmap, info->base + SC2731_CHG_CFG5, &val); + if (ret) + return ret; + + val = (val & SC2731_CUR_LIMIT_MASK) >> SC2731_CUR_LIMIT_SHIFT; + + switch (val) { + case 0: + *cur = SC2731_CURRENT_LIMIT_100; + break; + + case 1: + *cur = SC2731_CURRENT_LIMIT_2000; + break; + + case 2: + *cur = SC2731_CURRENT_LIMIT_900; + break; + + case 3: + *cur = SC2731_CURRENT_LIMIT_500; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int +sc2731_charger_usb_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct sc2731_charger_info *info = power_supply_get_drvdata(psy); + int ret; + + mutex_lock(&info->lock); + + if (!info->charging) { + mutex_unlock(&info->lock); + return -ENODEV; + } + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = sc2731_charger_set_current(info, val->intval / 1000); + if (ret < 0) + dev_err(info->dev, "set charge current failed\n"); + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = sc2731_charger_set_current_limit(info, + val->intval / 1000); + if (ret < 0) + dev_err(info->dev, "set input current limit failed\n"); + break; + + default: + ret = -EINVAL; + } + + mutex_unlock(&info->lock); + return ret; +} + +static int sc2731_charger_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sc2731_charger_info *info = power_supply_get_drvdata(psy); + int ret = 0; + u32 cur; + + mutex_lock(&info->lock); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (info->charging) + val->intval = sc2731_charger_get_status(info); + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + if (!info->charging) { + val->intval = 0; + } else { + ret = sc2731_charger_get_current(info, &cur); + if (ret) + goto out; + + val->intval = cur * 1000; + } + break; + + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (!info->charging) { + val->intval = 0; + } else { + ret = sc2731_charger_get_current_limit(info, &cur); + if (ret) + goto out; + + val->intval = cur * 1000; + } + break; + + default: + ret = -EINVAL; + } + +out: + mutex_unlock(&info->lock); + return ret; +} + +static int sc2731_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = 1; + break; + + default: + ret = 0; + } + + return ret; +} + +static enum power_supply_property sc2731_usb_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, +}; + +static const struct power_supply_desc sc2731_charger_desc = { + .name = "sc2731_charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = sc2731_usb_props, + .num_properties = ARRAY_SIZE(sc2731_usb_props), + .get_property = sc2731_charger_usb_get_property, + .set_property = sc2731_charger_usb_set_property, + .property_is_writeable = sc2731_charger_property_is_writeable, +}; + +static void sc2731_charger_work(struct work_struct *data) +{ + struct sc2731_charger_info *info = + container_of(data, struct sc2731_charger_info, work); + int ret; + + mutex_lock(&info->lock); + + if (info->limit > 0 && !info->charging) { + /* set current limitation and start to charge */ + ret = sc2731_charger_set_current_limit(info, info->limit); + if (ret) + goto out; + + ret = sc2731_charger_set_current(info, info->limit); + if (ret) + goto out; + + ret = sc2731_charger_start_charge(info); + if (ret) + goto out; + + info->charging = true; + } else if (!info->limit && info->charging) { + /* Stop charging */ + info->charging = false; + sc2731_charger_stop_charge(info); + } + +out: + mutex_unlock(&info->lock); +} + +static int sc2731_charger_usb_change(struct notifier_block *nb, + unsigned long limit, void *data) +{ + struct sc2731_charger_info *info = + container_of(nb, struct sc2731_charger_info, usb_notify); + + info->limit = limit; + + schedule_work(&info->work); + + return NOTIFY_OK; +} + +static int sc2731_charger_hw_init(struct sc2731_charger_info *info) +{ + struct power_supply_battery_info bat_info = { }; + u32 term_currrent, term_voltage, cur_val, vol_val; + int ret; + + /* Enable charger module */ + ret = regmap_update_bits(info->regmap, SC2731_MODULE_EN1, + SC2731_CHARGE_EN, SC2731_CHARGE_EN); + if (ret) + return ret; + + ret = power_supply_get_battery_info(info->psy_usb, &bat_info); + if (ret) { + dev_warn(info->dev, "no battery information is supplied\n"); + + /* + * If no battery information is supplied, we should set + * default charge termination current to 120 mA, and default + * charge termination voltage to 4.35V. + */ + cur_val = 0x2; + vol_val = 0x1; + } else { + term_currrent = bat_info.charge_term_current_ua / 1000; + + if (term_currrent <= 90) + cur_val = 0; + else if (term_currrent >= 265) + cur_val = 0x7; + else + cur_val = ((term_currrent - 90) / 25) + 1; + + term_voltage = bat_info.constant_charge_voltage_max_uv / 1000; + + if (term_voltage > 4500) + term_voltage = 4500; + + if (term_voltage > 4200) + vol_val = (term_voltage - 4200) / 100; + else + vol_val = 0; + + power_supply_put_battery_info(info->psy_usb, &bat_info); + } + + /* Set charge termination current */ + ret = regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG2, + SC2731_TERMINATION_CUR_MASK, cur_val); + if (ret) + goto error; + + /* Set charge termination voltage */ + ret = regmap_update_bits(info->regmap, info->base + SC2731_CHG_CFG0, + SC2731_TERMINATION_VOL_MASK | + SC2731_TERMINATION_VOL_CAL_MASK, + (vol_val << SC2731_TERMINATION_VOL_SHIFT) | + (0x6 << SC2731_TERMINATION_VOL_CAL_SHIFT)); + if (ret) + goto error; + + return 0; + +error: + regmap_update_bits(info->regmap, SC2731_MODULE_EN1, SC2731_CHARGE_EN, 0); + return ret; +} + +static void sc2731_charger_detect_status(struct sc2731_charger_info *info) +{ + unsigned int min, max; + + /* + * If the USB charger status has been USB_CHARGER_PRESENT before + * registering the notifier, we should start to charge with getting + * the charge current. + */ + if (info->usb_phy->chg_state != USB_CHARGER_PRESENT) + return; + + usb_phy_get_charger_current(info->usb_phy, &min, &max); + info->limit = min; + + schedule_work(&info->work); +} + +static int sc2731_charger_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct sc2731_charger_info *info; + struct power_supply_config charger_cfg = { }; + int ret; + + info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + mutex_init(&info->lock); + info->dev = &pdev->dev; + INIT_WORK(&info->work, sc2731_charger_work); + + info->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!info->regmap) { + dev_err(&pdev->dev, "failed to get charger regmap\n"); + return -ENODEV; + } + + ret = of_property_read_u32(np, "reg", &info->base); + if (ret) { + dev_err(&pdev->dev, "failed to get register address\n"); + return -ENODEV; + } + + charger_cfg.drv_data = info; + charger_cfg.of_node = np; + info->psy_usb = devm_power_supply_register(&pdev->dev, + &sc2731_charger_desc, + &charger_cfg); + if (IS_ERR(info->psy_usb)) { + dev_err(&pdev->dev, "failed to register power supply\n"); + return PTR_ERR(info->psy_usb); + } + + ret = sc2731_charger_hw_init(info); + if (ret) + return ret; + + info->usb_phy = devm_usb_get_phy_by_phandle(&pdev->dev, "phys", 0); + if (IS_ERR(info->usb_phy)) { + dev_err(&pdev->dev, "failed to find USB phy\n"); + return PTR_ERR(info->usb_phy); + } + + info->usb_notify.notifier_call = sc2731_charger_usb_change; + ret = usb_register_notifier(info->usb_phy, &info->usb_notify); + if (ret) { + dev_err(&pdev->dev, "failed to register notifier: %d\n", ret); + return ret; + } + + sc2731_charger_detect_status(info); + + return 0; +} + +static int sc2731_charger_remove(struct platform_device *pdev) +{ + struct sc2731_charger_info *info = platform_get_drvdata(pdev); + + usb_unregister_notifier(info->usb_phy, &info->usb_notify); + + return 0; +} + +static const struct of_device_id sc2731_charger_of_match[] = { + { .compatible = "sprd,sc2731-charger", }, + { } +}; +MODULE_DEVICE_TABLE(of, sc2731_charger_of_match); + +static struct platform_driver sc2731_charger_driver = { + .driver = { + .name = "sc2731-charger", + .of_match_table = sc2731_charger_of_match, + }, + .probe = sc2731_charger_probe, + .remove = sc2731_charger_remove, +}; + +module_platform_driver(sc2731_charger_driver); + +MODULE_DESCRIPTION("Spreadtrum SC2731 Charger Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/sc27xx_fuel_gauge.c b/drivers/power/supply/sc27xx_fuel_gauge.c new file mode 100644 index 000000000..3bf4b2639 --- /dev/null +++ b/drivers/power/supply/sc27xx_fuel_gauge.c @@ -0,0 +1,1352 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2018 Spreadtrum Communications Inc. + +#include <linux/gpio/consumer.h> +#include <linux/iio/consumer.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/math64.h> +#include <linux/module.h> +#include <linux/nvmem-consumer.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +/* PMIC global control registers definition */ +#define SC27XX_MODULE_EN0 0xc08 +#define SC27XX_CLK_EN0 0xc18 +#define SC27XX_FGU_EN BIT(7) +#define SC27XX_FGU_RTC_EN BIT(6) + +/* FGU registers definition */ +#define SC27XX_FGU_START 0x0 +#define SC27XX_FGU_CONFIG 0x4 +#define SC27XX_FGU_ADC_CONFIG 0x8 +#define SC27XX_FGU_STATUS 0xc +#define SC27XX_FGU_INT_EN 0x10 +#define SC27XX_FGU_INT_CLR 0x14 +#define SC27XX_FGU_INT_STS 0x1c +#define SC27XX_FGU_VOLTAGE 0x20 +#define SC27XX_FGU_OCV 0x24 +#define SC27XX_FGU_POCV 0x28 +#define SC27XX_FGU_CURRENT 0x2c +#define SC27XX_FGU_LOW_OVERLOAD 0x34 +#define SC27XX_FGU_CLBCNT_SETH 0x50 +#define SC27XX_FGU_CLBCNT_SETL 0x54 +#define SC27XX_FGU_CLBCNT_DELTH 0x58 +#define SC27XX_FGU_CLBCNT_DELTL 0x5c +#define SC27XX_FGU_CLBCNT_VALH 0x68 +#define SC27XX_FGU_CLBCNT_VALL 0x6c +#define SC27XX_FGU_CLBCNT_QMAXL 0x74 +#define SC27XX_FGU_USER_AREA_SET 0xa0 +#define SC27XX_FGU_USER_AREA_CLEAR 0xa4 +#define SC27XX_FGU_USER_AREA_STATUS 0xa8 +#define SC27XX_FGU_VOLTAGE_BUF 0xd0 +#define SC27XX_FGU_CURRENT_BUF 0xf0 + +#define SC27XX_WRITE_SELCLB_EN BIT(0) +#define SC27XX_FGU_CLBCNT_MASK GENMASK(15, 0) +#define SC27XX_FGU_CLBCNT_SHIFT 16 +#define SC27XX_FGU_LOW_OVERLOAD_MASK GENMASK(12, 0) + +#define SC27XX_FGU_INT_MASK GENMASK(9, 0) +#define SC27XX_FGU_LOW_OVERLOAD_INT BIT(0) +#define SC27XX_FGU_CLBCNT_DELTA_INT BIT(2) + +#define SC27XX_FGU_MODE_AREA_MASK GENMASK(15, 12) +#define SC27XX_FGU_CAP_AREA_MASK GENMASK(11, 0) +#define SC27XX_FGU_MODE_AREA_SHIFT 12 + +#define SC27XX_FGU_FIRST_POWERTON GENMASK(3, 0) +#define SC27XX_FGU_DEFAULT_CAP GENMASK(11, 0) +#define SC27XX_FGU_NORMAIL_POWERTON 0x5 + +#define SC27XX_FGU_CUR_BASIC_ADC 8192 +#define SC27XX_FGU_SAMPLE_HZ 2 +/* micro Ohms */ +#define SC27XX_FGU_IDEAL_RESISTANCE 20000 + +/* + * struct sc27xx_fgu_data: describe the FGU device + * @regmap: regmap for register access + * @dev: platform device + * @battery: battery power supply + * @base: the base offset for the controller + * @lock: protect the structure + * @gpiod: GPIO for battery detection + * @channel: IIO channel to get battery temperature + * @charge_chan: IIO channel to get charge voltage + * @internal_resist: the battery internal resistance in mOhm + * @total_cap: the total capacity of the battery in mAh + * @init_cap: the initial capacity of the battery in mAh + * @alarm_cap: the alarm capacity + * @init_clbcnt: the initial coulomb counter + * @max_volt: the maximum constant input voltage in millivolt + * @min_volt: the minimum drained battery voltage in microvolt + * @boot_volt: the voltage measured during boot in microvolt + * @table_len: the capacity table length + * @resist_table_len: the resistance table length + * @cur_1000ma_adc: ADC value corresponding to 1000 mA + * @vol_1000mv_adc: ADC value corresponding to 1000 mV + * @calib_resist: the real resistance of coulomb counter chip in uOhm + * @cap_table: capacity table with corresponding ocv + * @resist_table: resistance percent table with corresponding temperature + */ +struct sc27xx_fgu_data { + struct regmap *regmap; + struct device *dev; + struct power_supply *battery; + u32 base; + struct mutex lock; + struct gpio_desc *gpiod; + struct iio_channel *channel; + struct iio_channel *charge_chan; + bool bat_present; + int internal_resist; + int total_cap; + int init_cap; + int alarm_cap; + int init_clbcnt; + int max_volt; + int min_volt; + int boot_volt; + int table_len; + int resist_table_len; + int cur_1000ma_adc; + int vol_1000mv_adc; + int calib_resist; + struct power_supply_battery_ocv_table *cap_table; + struct power_supply_resistance_temp_table *resist_table; +}; + +static int sc27xx_fgu_cap_to_clbcnt(struct sc27xx_fgu_data *data, int capacity); +static void sc27xx_fgu_capacity_calibration(struct sc27xx_fgu_data *data, + int cap, bool int_mode); +static void sc27xx_fgu_adjust_cap(struct sc27xx_fgu_data *data, int cap); +static int sc27xx_fgu_get_temp(struct sc27xx_fgu_data *data, int *temp); + +static const char * const sc27xx_charger_supply_name[] = { + "sc2731_charger", + "sc2720_charger", + "sc2721_charger", + "sc2723_charger", +}; + +static int sc27xx_fgu_adc_to_current(struct sc27xx_fgu_data *data, s64 adc) +{ + return DIV_S64_ROUND_CLOSEST(adc * 1000, data->cur_1000ma_adc); +} + +static int sc27xx_fgu_adc_to_voltage(struct sc27xx_fgu_data *data, s64 adc) +{ + return DIV_S64_ROUND_CLOSEST(adc * 1000, data->vol_1000mv_adc); +} + +static int sc27xx_fgu_voltage_to_adc(struct sc27xx_fgu_data *data, int vol) +{ + return DIV_ROUND_CLOSEST(vol * data->vol_1000mv_adc, 1000); +} + +static bool sc27xx_fgu_is_first_poweron(struct sc27xx_fgu_data *data) +{ + int ret, status, cap, mode; + + ret = regmap_read(data->regmap, + data->base + SC27XX_FGU_USER_AREA_STATUS, &status); + if (ret) + return false; + + /* + * We use low 4 bits to save the last battery capacity and high 12 bits + * to save the system boot mode. + */ + mode = (status & SC27XX_FGU_MODE_AREA_MASK) >> SC27XX_FGU_MODE_AREA_SHIFT; + cap = status & SC27XX_FGU_CAP_AREA_MASK; + + /* + * When FGU has been powered down, the user area registers became + * default value (0xffff), which can be used to valid if the system is + * first power on or not. + */ + if (mode == SC27XX_FGU_FIRST_POWERTON || cap == SC27XX_FGU_DEFAULT_CAP) + return true; + + return false; +} + +static int sc27xx_fgu_save_boot_mode(struct sc27xx_fgu_data *data, + int boot_mode) +{ + int ret; + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_CLEAR, + SC27XX_FGU_MODE_AREA_MASK, + SC27XX_FGU_MODE_AREA_MASK); + if (ret) + return ret; + + /* + * Since the user area registers are put on power always-on region, + * then these registers changing time will be a little long. Thus + * here we should delay 200us to wait until values are updated + * successfully according to the datasheet. + */ + udelay(200); + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_SET, + SC27XX_FGU_MODE_AREA_MASK, + boot_mode << SC27XX_FGU_MODE_AREA_SHIFT); + if (ret) + return ret; + + /* + * Since the user area registers are put on power always-on region, + * then these registers changing time will be a little long. Thus + * here we should delay 200us to wait until values are updated + * successfully according to the datasheet. + */ + udelay(200); + + /* + * According to the datasheet, we should set the USER_AREA_CLEAR to 0 to + * make the user area data available, otherwise we can not save the user + * area data. + */ + return regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_CLEAR, + SC27XX_FGU_MODE_AREA_MASK, 0); +} + +static int sc27xx_fgu_save_last_cap(struct sc27xx_fgu_data *data, int cap) +{ + int ret; + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_CLEAR, + SC27XX_FGU_CAP_AREA_MASK, + SC27XX_FGU_CAP_AREA_MASK); + if (ret) + return ret; + + /* + * Since the user area registers are put on power always-on region, + * then these registers changing time will be a little long. Thus + * here we should delay 200us to wait until values are updated + * successfully according to the datasheet. + */ + udelay(200); + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_SET, + SC27XX_FGU_CAP_AREA_MASK, cap); + if (ret) + return ret; + + /* + * Since the user area registers are put on power always-on region, + * then these registers changing time will be a little long. Thus + * here we should delay 200us to wait until values are updated + * successfully according to the datasheet. + */ + udelay(200); + + /* + * According to the datasheet, we should set the USER_AREA_CLEAR to 0 to + * make the user area data available, otherwise we can not save the user + * area data. + */ + return regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_USER_AREA_CLEAR, + SC27XX_FGU_CAP_AREA_MASK, 0); +} + +static int sc27xx_fgu_read_last_cap(struct sc27xx_fgu_data *data, int *cap) +{ + int ret, value; + + ret = regmap_read(data->regmap, + data->base + SC27XX_FGU_USER_AREA_STATUS, &value); + if (ret) + return ret; + + *cap = value & SC27XX_FGU_CAP_AREA_MASK; + return 0; +} + +/* + * When system boots on, we can not read battery capacity from coulomb + * registers, since now the coulomb registers are invalid. So we should + * calculate the battery open circuit voltage, and get current battery + * capacity according to the capacity table. + */ +static int sc27xx_fgu_get_boot_capacity(struct sc27xx_fgu_data *data, int *cap) +{ + int volt, cur, oci, ocv, ret; + bool is_first_poweron = sc27xx_fgu_is_first_poweron(data); + + /* + * If system is not the first power on, we should use the last saved + * battery capacity as the initial battery capacity. Otherwise we should + * re-calculate the initial battery capacity. + */ + if (!is_first_poweron) { + ret = sc27xx_fgu_read_last_cap(data, cap); + if (ret) + return ret; + + return sc27xx_fgu_save_boot_mode(data, SC27XX_FGU_NORMAIL_POWERTON); + } + + /* + * After system booting on, the SC27XX_FGU_CLBCNT_QMAXL register saved + * the first sampled open circuit current. + */ + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_CLBCNT_QMAXL, + &cur); + if (ret) + return ret; + + cur <<= 1; + oci = sc27xx_fgu_adc_to_current(data, cur - SC27XX_FGU_CUR_BASIC_ADC); + + /* + * Should get the OCV from SC27XX_FGU_POCV register at the system + * beginning. It is ADC values reading from registers which need to + * convert the corresponding voltage. + */ + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_POCV, &volt); + if (ret) + return ret; + + volt = sc27xx_fgu_adc_to_voltage(data, volt); + ocv = volt * 1000 - oci * data->internal_resist; + data->boot_volt = ocv; + + /* + * Parse the capacity table to look up the correct capacity percent + * according to current battery's corresponding OCV values. + */ + *cap = power_supply_ocv2cap_simple(data->cap_table, data->table_len, + ocv); + + ret = sc27xx_fgu_save_last_cap(data, *cap); + if (ret) + return ret; + + return sc27xx_fgu_save_boot_mode(data, SC27XX_FGU_NORMAIL_POWERTON); +} + +static int sc27xx_fgu_set_clbcnt(struct sc27xx_fgu_data *data, int clbcnt) +{ + int ret; + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_CLBCNT_SETL, + SC27XX_FGU_CLBCNT_MASK, clbcnt); + if (ret) + return ret; + + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_CLBCNT_SETH, + SC27XX_FGU_CLBCNT_MASK, + clbcnt >> SC27XX_FGU_CLBCNT_SHIFT); + if (ret) + return ret; + + return regmap_update_bits(data->regmap, data->base + SC27XX_FGU_START, + SC27XX_WRITE_SELCLB_EN, + SC27XX_WRITE_SELCLB_EN); +} + +static int sc27xx_fgu_get_clbcnt(struct sc27xx_fgu_data *data, int *clb_cnt) +{ + int ccl, cch, ret; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_CLBCNT_VALL, + &ccl); + if (ret) + return ret; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_CLBCNT_VALH, + &cch); + if (ret) + return ret; + + *clb_cnt = ccl & SC27XX_FGU_CLBCNT_MASK; + *clb_cnt |= (cch & SC27XX_FGU_CLBCNT_MASK) << SC27XX_FGU_CLBCNT_SHIFT; + + return 0; +} + +static int sc27xx_fgu_get_vol_now(struct sc27xx_fgu_data *data, int *val) +{ + int ret; + u32 vol; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_VOLTAGE_BUF, + &vol); + if (ret) + return ret; + + /* + * It is ADC values reading from registers which need to convert to + * corresponding voltage values. + */ + *val = sc27xx_fgu_adc_to_voltage(data, vol); + + return 0; +} + +static int sc27xx_fgu_get_cur_now(struct sc27xx_fgu_data *data, int *val) +{ + int ret; + u32 cur; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_CURRENT_BUF, + &cur); + if (ret) + return ret; + + /* + * It is ADC values reading from registers which need to convert to + * corresponding current values. + */ + *val = sc27xx_fgu_adc_to_current(data, cur - SC27XX_FGU_CUR_BASIC_ADC); + + return 0; +} + +static int sc27xx_fgu_get_capacity(struct sc27xx_fgu_data *data, int *cap) +{ + int ret, cur_clbcnt, delta_clbcnt, delta_cap, temp; + + /* Get current coulomb counters firstly */ + ret = sc27xx_fgu_get_clbcnt(data, &cur_clbcnt); + if (ret) + return ret; + + delta_clbcnt = cur_clbcnt - data->init_clbcnt; + + /* + * Convert coulomb counter to delta capacity (mAh), and set multiplier + * as 10 to improve the precision. + */ + temp = DIV_ROUND_CLOSEST(delta_clbcnt * 10, 36 * SC27XX_FGU_SAMPLE_HZ); + temp = sc27xx_fgu_adc_to_current(data, temp / 1000); + + /* + * Convert to capacity percent of the battery total capacity, + * and multiplier is 100 too. + */ + delta_cap = DIV_ROUND_CLOSEST(temp * 100, data->total_cap); + *cap = delta_cap + data->init_cap; + + /* Calibrate the battery capacity in a normal range. */ + sc27xx_fgu_capacity_calibration(data, *cap, false); + + return 0; +} + +static int sc27xx_fgu_get_vbat_vol(struct sc27xx_fgu_data *data, int *val) +{ + int ret, vol; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_VOLTAGE, &vol); + if (ret) + return ret; + + /* + * It is ADC values reading from registers which need to convert to + * corresponding voltage values. + */ + *val = sc27xx_fgu_adc_to_voltage(data, vol); + + return 0; +} + +static int sc27xx_fgu_get_current(struct sc27xx_fgu_data *data, int *val) +{ + int ret, cur; + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_CURRENT, &cur); + if (ret) + return ret; + + /* + * It is ADC values reading from registers which need to convert to + * corresponding current values. + */ + *val = sc27xx_fgu_adc_to_current(data, cur - SC27XX_FGU_CUR_BASIC_ADC); + + return 0; +} + +static int sc27xx_fgu_get_vbat_ocv(struct sc27xx_fgu_data *data, int *val) +{ + int vol, cur, ret, temp, resistance; + + ret = sc27xx_fgu_get_vbat_vol(data, &vol); + if (ret) + return ret; + + ret = sc27xx_fgu_get_current(data, &cur); + if (ret) + return ret; + + resistance = data->internal_resist; + if (data->resist_table_len > 0) { + ret = sc27xx_fgu_get_temp(data, &temp); + if (ret) + return ret; + + resistance = power_supply_temp2resist_simple(data->resist_table, + data->resist_table_len, temp); + resistance = data->internal_resist * resistance / 100; + } + + /* Return the battery OCV in micro volts. */ + *val = vol * 1000 - cur * resistance; + + return 0; +} + +static int sc27xx_fgu_get_charge_vol(struct sc27xx_fgu_data *data, int *val) +{ + int ret, vol; + + ret = iio_read_channel_processed(data->charge_chan, &vol); + if (ret < 0) + return ret; + + *val = vol * 1000; + return 0; +} + +static int sc27xx_fgu_get_temp(struct sc27xx_fgu_data *data, int *temp) +{ + return iio_read_channel_processed(data->channel, temp); +} + +static int sc27xx_fgu_get_health(struct sc27xx_fgu_data *data, int *health) +{ + int ret, vol; + + ret = sc27xx_fgu_get_vbat_vol(data, &vol); + if (ret) + return ret; + + if (vol > data->max_volt) + *health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + *health = POWER_SUPPLY_HEALTH_GOOD; + + return 0; +} + +static int sc27xx_fgu_get_status(struct sc27xx_fgu_data *data, int *status) +{ + union power_supply_propval val; + struct power_supply *psy; + int i, ret = -EINVAL; + + for (i = 0; i < ARRAY_SIZE(sc27xx_charger_supply_name); i++) { + psy = power_supply_get_by_name(sc27xx_charger_supply_name[i]); + if (!psy) + continue; + + ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_STATUS, + &val); + power_supply_put(psy); + if (ret) + return ret; + + *status = val.intval; + } + + return ret; +} + +static int sc27xx_fgu_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct sc27xx_fgu_data *data = power_supply_get_drvdata(psy); + int ret = 0; + int value; + + mutex_lock(&data->lock); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = sc27xx_fgu_get_status(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_HEALTH: + ret = sc27xx_fgu_get_health(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_PRESENT: + val->intval = data->bat_present; + break; + + case POWER_SUPPLY_PROP_TEMP: + ret = sc27xx_fgu_get_temp(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + + case POWER_SUPPLY_PROP_CAPACITY: + ret = sc27xx_fgu_get_capacity(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + ret = sc27xx_fgu_get_vbat_vol(data, &value); + if (ret) + goto error; + + val->intval = value * 1000; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + ret = sc27xx_fgu_get_vbat_ocv(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = sc27xx_fgu_get_charge_vol(data, &value); + if (ret) + goto error; + + val->intval = value; + break; + + case POWER_SUPPLY_PROP_CURRENT_AVG: + ret = sc27xx_fgu_get_current(data, &value); + if (ret) + goto error; + + val->intval = value * 1000; + break; + + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + val->intval = data->total_cap * 1000; + break; + + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = sc27xx_fgu_get_clbcnt(data, &value); + if (ret) + goto error; + + value = DIV_ROUND_CLOSEST(value * 10, + 36 * SC27XX_FGU_SAMPLE_HZ); + val->intval = sc27xx_fgu_adc_to_current(data, value); + + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = sc27xx_fgu_get_vol_now(data, &value); + if (ret) + goto error; + + val->intval = value * 1000; + break; + + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = sc27xx_fgu_get_cur_now(data, &value); + if (ret) + goto error; + + val->intval = value * 1000; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_BOOT: + val->intval = data->boot_volt; + break; + + default: + ret = -EINVAL; + break; + } + +error: + mutex_unlock(&data->lock); + return ret; +} + +static int sc27xx_fgu_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct sc27xx_fgu_data *data = power_supply_get_drvdata(psy); + int ret; + + mutex_lock(&data->lock); + + switch (psp) { + case POWER_SUPPLY_PROP_CAPACITY: + ret = sc27xx_fgu_save_last_cap(data, val->intval); + if (ret < 0) + dev_err(data->dev, "failed to save battery capacity\n"); + break; + + case POWER_SUPPLY_PROP_CALIBRATE: + sc27xx_fgu_adjust_cap(data, val->intval); + ret = 0; + break; + + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + data->total_cap = val->intval / 1000; + ret = 0; + break; + + default: + ret = -EINVAL; + } + + mutex_unlock(&data->lock); + + return ret; +} + +static int sc27xx_fgu_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return psp == POWER_SUPPLY_PROP_CAPACITY || + psp == POWER_SUPPLY_PROP_CALIBRATE || + psp == POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN; +} + +static enum power_supply_property sc27xx_fgu_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_OCV, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_VOLTAGE_BOOT, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, + POWER_SUPPLY_PROP_CALIBRATE, + POWER_SUPPLY_PROP_CHARGE_NOW +}; + +static const struct power_supply_desc sc27xx_fgu_desc = { + .name = "sc27xx-fgu", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = sc27xx_fgu_props, + .num_properties = ARRAY_SIZE(sc27xx_fgu_props), + .get_property = sc27xx_fgu_get_property, + .set_property = sc27xx_fgu_set_property, + .external_power_changed = power_supply_changed, + .property_is_writeable = sc27xx_fgu_property_is_writeable, + .no_thermal = true, +}; + +static void sc27xx_fgu_adjust_cap(struct sc27xx_fgu_data *data, int cap) +{ + int ret; + + data->init_cap = cap; + ret = sc27xx_fgu_get_clbcnt(data, &data->init_clbcnt); + if (ret) + dev_err(data->dev, "failed to get init coulomb counter\n"); +} + +static void sc27xx_fgu_capacity_calibration(struct sc27xx_fgu_data *data, + int cap, bool int_mode) +{ + int ret, ocv, chg_sts, adc; + + ret = sc27xx_fgu_get_vbat_ocv(data, &ocv); + if (ret) { + dev_err(data->dev, "get battery ocv error.\n"); + return; + } + + ret = sc27xx_fgu_get_status(data, &chg_sts); + if (ret) { + dev_err(data->dev, "get charger status error.\n"); + return; + } + + /* + * If we are in charging mode, then we do not need to calibrate the + * lower capacity. + */ + if (chg_sts == POWER_SUPPLY_STATUS_CHARGING) + return; + + if ((ocv > data->cap_table[0].ocv && cap < 100) || cap > 100) { + /* + * If current OCV value is larger than the max OCV value in + * OCV table, or the current capacity is larger than 100, + * we should force the inititial capacity to 100. + */ + sc27xx_fgu_adjust_cap(data, 100); + } else if (ocv <= data->cap_table[data->table_len - 1].ocv) { + /* + * If current OCV value is leass than the minimum OCV value in + * OCV table, we should force the inititial capacity to 0. + */ + sc27xx_fgu_adjust_cap(data, 0); + } else if ((ocv > data->cap_table[data->table_len - 1].ocv && cap <= 0) || + (ocv > data->min_volt && cap <= data->alarm_cap)) { + /* + * If current OCV value is not matchable with current capacity, + * we should re-calculate current capacity by looking up the + * OCV table. + */ + int cur_cap = power_supply_ocv2cap_simple(data->cap_table, + data->table_len, ocv); + + sc27xx_fgu_adjust_cap(data, cur_cap); + } else if (ocv <= data->min_volt) { + /* + * If current OCV value is less than the low alarm voltage, but + * current capacity is larger than the alarm capacity, we should + * adjust the inititial capacity to alarm capacity. + */ + if (cap > data->alarm_cap) { + sc27xx_fgu_adjust_cap(data, data->alarm_cap); + } else { + int cur_cap; + + /* + * If current capacity is equal with 0 or less than 0 + * (some error occurs), we should adjust inititial + * capacity to the capacity corresponding to current OCV + * value. + */ + cur_cap = power_supply_ocv2cap_simple(data->cap_table, + data->table_len, + ocv); + sc27xx_fgu_adjust_cap(data, cur_cap); + } + + if (!int_mode) + return; + + /* + * After adjusting the battery capacity, we should set the + * lowest alarm voltage instead. + */ + data->min_volt = data->cap_table[data->table_len - 1].ocv; + data->alarm_cap = power_supply_ocv2cap_simple(data->cap_table, + data->table_len, + data->min_volt); + + adc = sc27xx_fgu_voltage_to_adc(data, data->min_volt / 1000); + regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_LOW_OVERLOAD, + SC27XX_FGU_LOW_OVERLOAD_MASK, adc); + } +} + +static irqreturn_t sc27xx_fgu_interrupt(int irq, void *dev_id) +{ + struct sc27xx_fgu_data *data = dev_id; + int ret, cap; + u32 status; + + mutex_lock(&data->lock); + + ret = regmap_read(data->regmap, data->base + SC27XX_FGU_INT_STS, + &status); + if (ret) + goto out; + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_INT_CLR, + status, status); + if (ret) + goto out; + + /* + * When low overload voltage interrupt happens, we should calibrate the + * battery capacity in lower voltage stage. + */ + if (!(status & SC27XX_FGU_LOW_OVERLOAD_INT)) + goto out; + + ret = sc27xx_fgu_get_capacity(data, &cap); + if (ret) + goto out; + + sc27xx_fgu_capacity_calibration(data, cap, true); + +out: + mutex_unlock(&data->lock); + + power_supply_changed(data->battery); + return IRQ_HANDLED; +} + +static irqreturn_t sc27xx_fgu_bat_detection(int irq, void *dev_id) +{ + struct sc27xx_fgu_data *data = dev_id; + int state; + + mutex_lock(&data->lock); + + state = gpiod_get_value_cansleep(data->gpiod); + if (state < 0) { + dev_err(data->dev, "failed to get gpio state\n"); + mutex_unlock(&data->lock); + return IRQ_RETVAL(state); + } + + data->bat_present = !!state; + + mutex_unlock(&data->lock); + + power_supply_changed(data->battery); + return IRQ_HANDLED; +} + +static void sc27xx_fgu_disable(void *_data) +{ + struct sc27xx_fgu_data *data = _data; + + regmap_update_bits(data->regmap, SC27XX_CLK_EN0, SC27XX_FGU_RTC_EN, 0); + regmap_update_bits(data->regmap, SC27XX_MODULE_EN0, SC27XX_FGU_EN, 0); +} + +static int sc27xx_fgu_cap_to_clbcnt(struct sc27xx_fgu_data *data, int capacity) +{ + /* + * Get current capacity (mAh) = battery total capacity (mAh) * + * current capacity percent (capacity / 100). + */ + int cur_cap = DIV_ROUND_CLOSEST(data->total_cap * capacity, 100); + + /* + * Convert current capacity (mAh) to coulomb counter according to the + * formula: 1 mAh =3.6 coulomb. + */ + return DIV_ROUND_CLOSEST(cur_cap * 36 * data->cur_1000ma_adc * SC27XX_FGU_SAMPLE_HZ, 10); +} + +static int sc27xx_fgu_calibration(struct sc27xx_fgu_data *data) +{ + struct nvmem_cell *cell; + int calib_data, cal_4200mv; + void *buf; + size_t len; + + cell = nvmem_cell_get(data->dev, "fgu_calib"); + if (IS_ERR(cell)) + return PTR_ERR(cell); + + buf = nvmem_cell_read(cell, &len); + nvmem_cell_put(cell); + + if (IS_ERR(buf)) + return PTR_ERR(buf); + + memcpy(&calib_data, buf, min(len, sizeof(u32))); + + /* + * Get the ADC value corresponding to 4200 mV from eFuse controller + * according to below formula. Then convert to ADC values corresponding + * to 1000 mV and 1000 mA. + */ + cal_4200mv = (calib_data & 0x1ff) + 6963 - 4096 - 256; + data->vol_1000mv_adc = DIV_ROUND_CLOSEST(cal_4200mv * 10, 42); + data->cur_1000ma_adc = + DIV_ROUND_CLOSEST(data->vol_1000mv_adc * 4 * data->calib_resist, + SC27XX_FGU_IDEAL_RESISTANCE); + + kfree(buf); + return 0; +} + +static int sc27xx_fgu_hw_init(struct sc27xx_fgu_data *data) +{ + struct power_supply_battery_info info = { }; + struct power_supply_battery_ocv_table *table; + int ret, delta_clbcnt, alarm_adc; + + ret = power_supply_get_battery_info(data->battery, &info); + if (ret) { + dev_err(data->dev, "failed to get battery information\n"); + return ret; + } + + data->total_cap = info.charge_full_design_uah / 1000; + data->max_volt = info.constant_charge_voltage_max_uv / 1000; + data->internal_resist = info.factory_internal_resistance_uohm / 1000; + data->min_volt = info.voltage_min_design_uv; + + /* + * For SC27XX fuel gauge device, we only use one ocv-capacity + * table in normal temperature 20 Celsius. + */ + table = power_supply_find_ocv2cap_table(&info, 20, &data->table_len); + if (!table) + return -EINVAL; + + data->cap_table = devm_kmemdup(data->dev, table, + data->table_len * sizeof(*table), + GFP_KERNEL); + if (!data->cap_table) { + power_supply_put_battery_info(data->battery, &info); + return -ENOMEM; + } + + data->alarm_cap = power_supply_ocv2cap_simple(data->cap_table, + data->table_len, + data->min_volt); + if (!data->alarm_cap) + data->alarm_cap += 1; + + data->resist_table_len = info.resist_table_size; + if (data->resist_table_len > 0) { + data->resist_table = devm_kmemdup(data->dev, info.resist_table, + data->resist_table_len * + sizeof(struct power_supply_resistance_temp_table), + GFP_KERNEL); + if (!data->resist_table) { + power_supply_put_battery_info(data->battery, &info); + return -ENOMEM; + } + } + + power_supply_put_battery_info(data->battery, &info); + + ret = sc27xx_fgu_calibration(data); + if (ret) + return ret; + + /* Enable the FGU module */ + ret = regmap_update_bits(data->regmap, SC27XX_MODULE_EN0, + SC27XX_FGU_EN, SC27XX_FGU_EN); + if (ret) { + dev_err(data->dev, "failed to enable fgu\n"); + return ret; + } + + /* Enable the FGU RTC clock to make it work */ + ret = regmap_update_bits(data->regmap, SC27XX_CLK_EN0, + SC27XX_FGU_RTC_EN, SC27XX_FGU_RTC_EN); + if (ret) { + dev_err(data->dev, "failed to enable fgu RTC clock\n"); + goto disable_fgu; + } + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_INT_CLR, + SC27XX_FGU_INT_MASK, SC27XX_FGU_INT_MASK); + if (ret) { + dev_err(data->dev, "failed to clear interrupt status\n"); + goto disable_clk; + } + + /* + * Set the voltage low overload threshold, which means when the battery + * voltage is lower than this threshold, the controller will generate + * one interrupt to notify. + */ + alarm_adc = sc27xx_fgu_voltage_to_adc(data, data->min_volt / 1000); + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_LOW_OVERLOAD, + SC27XX_FGU_LOW_OVERLOAD_MASK, alarm_adc); + if (ret) { + dev_err(data->dev, "failed to set fgu low overload\n"); + goto disable_clk; + } + + /* + * Set the coulomb counter delta threshold, that means when the coulomb + * counter change is multiples of the delta threshold, the controller + * will generate one interrupt to notify the users to update the battery + * capacity. Now we set the delta threshold as a counter value of 1% + * capacity. + */ + delta_clbcnt = sc27xx_fgu_cap_to_clbcnt(data, 1); + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_CLBCNT_DELTL, + SC27XX_FGU_CLBCNT_MASK, delta_clbcnt); + if (ret) { + dev_err(data->dev, "failed to set low delta coulomb counter\n"); + goto disable_clk; + } + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_CLBCNT_DELTH, + SC27XX_FGU_CLBCNT_MASK, + delta_clbcnt >> SC27XX_FGU_CLBCNT_SHIFT); + if (ret) { + dev_err(data->dev, "failed to set high delta coulomb counter\n"); + goto disable_clk; + } + + /* + * Get the boot battery capacity when system powers on, which is used to + * initialize the coulomb counter. After that, we can read the coulomb + * counter to measure the battery capacity. + */ + ret = sc27xx_fgu_get_boot_capacity(data, &data->init_cap); + if (ret) { + dev_err(data->dev, "failed to get boot capacity\n"); + goto disable_clk; + } + + /* + * Convert battery capacity to the corresponding initial coulomb counter + * and set into coulomb counter registers. + */ + data->init_clbcnt = sc27xx_fgu_cap_to_clbcnt(data, data->init_cap); + ret = sc27xx_fgu_set_clbcnt(data, data->init_clbcnt); + if (ret) { + dev_err(data->dev, "failed to initialize coulomb counter\n"); + goto disable_clk; + } + + return 0; + +disable_clk: + regmap_update_bits(data->regmap, SC27XX_CLK_EN0, SC27XX_FGU_RTC_EN, 0); +disable_fgu: + regmap_update_bits(data->regmap, SC27XX_MODULE_EN0, SC27XX_FGU_EN, 0); + + return ret; +} + +static int sc27xx_fgu_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + struct power_supply_config fgu_cfg = { }; + struct sc27xx_fgu_data *data; + int ret, irq; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->regmap = dev_get_regmap(dev->parent, NULL); + if (!data->regmap) { + dev_err(dev, "failed to get regmap\n"); + return -ENODEV; + } + + ret = device_property_read_u32(dev, "reg", &data->base); + if (ret) { + dev_err(dev, "failed to get fgu address\n"); + return ret; + } + + ret = device_property_read_u32(&pdev->dev, + "sprd,calib-resistance-micro-ohms", + &data->calib_resist); + if (ret) { + dev_err(&pdev->dev, + "failed to get fgu calibration resistance\n"); + return ret; + } + + data->channel = devm_iio_channel_get(dev, "bat-temp"); + if (IS_ERR(data->channel)) { + dev_err(dev, "failed to get IIO channel\n"); + return PTR_ERR(data->channel); + } + + data->charge_chan = devm_iio_channel_get(dev, "charge-vol"); + if (IS_ERR(data->charge_chan)) { + dev_err(dev, "failed to get charge IIO channel\n"); + return PTR_ERR(data->charge_chan); + } + + data->gpiod = devm_gpiod_get(dev, "bat-detect", GPIOD_IN); + if (IS_ERR(data->gpiod)) { + dev_err(dev, "failed to get battery detection GPIO\n"); + return PTR_ERR(data->gpiod); + } + + ret = gpiod_get_value_cansleep(data->gpiod); + if (ret < 0) { + dev_err(dev, "failed to get gpio state\n"); + return ret; + } + + data->bat_present = !!ret; + mutex_init(&data->lock); + data->dev = dev; + platform_set_drvdata(pdev, data); + + fgu_cfg.drv_data = data; + fgu_cfg.of_node = np; + data->battery = devm_power_supply_register(dev, &sc27xx_fgu_desc, + &fgu_cfg); + if (IS_ERR(data->battery)) { + dev_err(dev, "failed to register power supply\n"); + return PTR_ERR(data->battery); + } + + ret = sc27xx_fgu_hw_init(data); + if (ret) { + dev_err(dev, "failed to initialize fgu hardware\n"); + return ret; + } + + ret = devm_add_action_or_reset(dev, sc27xx_fgu_disable, data); + if (ret) { + dev_err(dev, "failed to add fgu disable action\n"); + return ret; + } + + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(dev, "no irq resource specified\n"); + return irq; + } + + ret = devm_request_threaded_irq(data->dev, irq, NULL, + sc27xx_fgu_interrupt, + IRQF_NO_SUSPEND | IRQF_ONESHOT, + pdev->name, data); + if (ret) { + dev_err(data->dev, "failed to request fgu IRQ\n"); + return ret; + } + + irq = gpiod_to_irq(data->gpiod); + if (irq < 0) { + dev_err(dev, "failed to translate GPIO to IRQ\n"); + return irq; + } + + ret = devm_request_threaded_irq(dev, irq, NULL, + sc27xx_fgu_bat_detection, + IRQF_ONESHOT | IRQF_TRIGGER_RISING | + IRQF_TRIGGER_FALLING, + pdev->name, data); + if (ret) { + dev_err(dev, "failed to request IRQ\n"); + return ret; + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int sc27xx_fgu_resume(struct device *dev) +{ + struct sc27xx_fgu_data *data = dev_get_drvdata(dev); + int ret; + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_INT_EN, + SC27XX_FGU_LOW_OVERLOAD_INT | + SC27XX_FGU_CLBCNT_DELTA_INT, 0); + if (ret) { + dev_err(data->dev, "failed to disable fgu interrupts\n"); + return ret; + } + + return 0; +} + +static int sc27xx_fgu_suspend(struct device *dev) +{ + struct sc27xx_fgu_data *data = dev_get_drvdata(dev); + int ret, status, ocv; + + ret = sc27xx_fgu_get_status(data, &status); + if (ret) + return ret; + + /* + * If we are charging, then no need to enable the FGU interrupts to + * adjust the battery capacity. + */ + if (status != POWER_SUPPLY_STATUS_NOT_CHARGING && + status != POWER_SUPPLY_STATUS_DISCHARGING) + return 0; + + ret = regmap_update_bits(data->regmap, data->base + SC27XX_FGU_INT_EN, + SC27XX_FGU_LOW_OVERLOAD_INT, + SC27XX_FGU_LOW_OVERLOAD_INT); + if (ret) { + dev_err(data->dev, "failed to enable low voltage interrupt\n"); + return ret; + } + + ret = sc27xx_fgu_get_vbat_ocv(data, &ocv); + if (ret) + goto disable_int; + + /* + * If current OCV is less than the minimum voltage, we should enable the + * coulomb counter threshold interrupt to notify events to adjust the + * battery capacity. + */ + if (ocv < data->min_volt) { + ret = regmap_update_bits(data->regmap, + data->base + SC27XX_FGU_INT_EN, + SC27XX_FGU_CLBCNT_DELTA_INT, + SC27XX_FGU_CLBCNT_DELTA_INT); + if (ret) { + dev_err(data->dev, + "failed to enable coulomb threshold int\n"); + goto disable_int; + } + } + + return 0; + +disable_int: + regmap_update_bits(data->regmap, data->base + SC27XX_FGU_INT_EN, + SC27XX_FGU_LOW_OVERLOAD_INT, 0); + return ret; +} +#endif + +static const struct dev_pm_ops sc27xx_fgu_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(sc27xx_fgu_suspend, sc27xx_fgu_resume) +}; + +static const struct of_device_id sc27xx_fgu_of_match[] = { + { .compatible = "sprd,sc2731-fgu", }, + { } +}; +MODULE_DEVICE_TABLE(of, sc27xx_fgu_of_match); + +static struct platform_driver sc27xx_fgu_driver = { + .probe = sc27xx_fgu_probe, + .driver = { + .name = "sc27xx-fgu", + .of_match_table = sc27xx_fgu_of_match, + .pm = &sc27xx_fgu_pm_ops, + } +}; + +module_platform_driver(sc27xx_fgu_driver); + +MODULE_DESCRIPTION("Spreadtrum SC27XX PMICs Fual Gauge Unit Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/smb347-charger.c b/drivers/power/supply/smb347-charger.c new file mode 100644 index 000000000..912e2184f --- /dev/null +++ b/drivers/power/supply/smb347-charger.c @@ -0,0 +1,1410 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Summit Microelectronics SMB347 Battery Charger Driver + * + * Copyright (C) 2011, Intel Corporation + * + * Authors: Bruce E. Robertson <bruce.e.robertson@intel.com> + * Mika Westerberg <mika.westerberg@linux.intel.com> + */ + +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/gpio.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/i2c.h> +#include <linux/power_supply.h> +#include <linux/property.h> +#include <linux/regmap.h> + +#include <dt-bindings/power/summit,smb347-charger.h> + +/* Use the default compensation method */ +#define SMB3XX_SOFT_TEMP_COMPENSATE_DEFAULT -1 + +/* Use default factory programmed value for hard/soft temperature limit */ +#define SMB3XX_TEMP_USE_DEFAULT -273 + +/* + * Configuration registers. These are mirrored to volatile RAM and can be + * written once %CMD_A_ALLOW_WRITE is set in %CMD_A register. They will be + * reloaded from non-volatile registers after POR. + */ +#define CFG_CHARGE_CURRENT 0x00 +#define CFG_CHARGE_CURRENT_FCC_MASK 0xe0 +#define CFG_CHARGE_CURRENT_FCC_SHIFT 5 +#define CFG_CHARGE_CURRENT_PCC_MASK 0x18 +#define CFG_CHARGE_CURRENT_PCC_SHIFT 3 +#define CFG_CHARGE_CURRENT_TC_MASK 0x07 +#define CFG_CURRENT_LIMIT 0x01 +#define CFG_CURRENT_LIMIT_DC_MASK 0xf0 +#define CFG_CURRENT_LIMIT_DC_SHIFT 4 +#define CFG_CURRENT_LIMIT_USB_MASK 0x0f +#define CFG_FLOAT_VOLTAGE 0x03 +#define CFG_FLOAT_VOLTAGE_FLOAT_MASK 0x3f +#define CFG_FLOAT_VOLTAGE_THRESHOLD_MASK 0xc0 +#define CFG_FLOAT_VOLTAGE_THRESHOLD_SHIFT 6 +#define CFG_STAT 0x05 +#define CFG_STAT_DISABLED BIT(5) +#define CFG_STAT_ACTIVE_HIGH BIT(7) +#define CFG_PIN 0x06 +#define CFG_PIN_EN_CTRL_MASK 0x60 +#define CFG_PIN_EN_CTRL_ACTIVE_HIGH 0x40 +#define CFG_PIN_EN_CTRL_ACTIVE_LOW 0x60 +#define CFG_PIN_EN_APSD_IRQ BIT(1) +#define CFG_PIN_EN_CHARGER_ERROR BIT(2) +#define CFG_PIN_EN_CTRL BIT(4) +#define CFG_THERM 0x07 +#define CFG_THERM_SOFT_HOT_COMPENSATION_MASK 0x03 +#define CFG_THERM_SOFT_HOT_COMPENSATION_SHIFT 0 +#define CFG_THERM_SOFT_COLD_COMPENSATION_MASK 0x0c +#define CFG_THERM_SOFT_COLD_COMPENSATION_SHIFT 2 +#define CFG_THERM_MONITOR_DISABLED BIT(4) +#define CFG_SYSOK 0x08 +#define CFG_SYSOK_SUSPEND_HARD_LIMIT_DISABLED BIT(2) +#define CFG_OTHER 0x09 +#define CFG_OTHER_RID_MASK 0xc0 +#define CFG_OTHER_RID_ENABLED_AUTO_OTG 0xc0 +#define CFG_OTG 0x0a +#define CFG_OTG_TEMP_THRESHOLD_MASK 0x30 +#define CFG_OTG_TEMP_THRESHOLD_SHIFT 4 +#define CFG_OTG_CC_COMPENSATION_MASK 0xc0 +#define CFG_OTG_CC_COMPENSATION_SHIFT 6 +#define CFG_TEMP_LIMIT 0x0b +#define CFG_TEMP_LIMIT_SOFT_HOT_MASK 0x03 +#define CFG_TEMP_LIMIT_SOFT_HOT_SHIFT 0 +#define CFG_TEMP_LIMIT_SOFT_COLD_MASK 0x0c +#define CFG_TEMP_LIMIT_SOFT_COLD_SHIFT 2 +#define CFG_TEMP_LIMIT_HARD_HOT_MASK 0x30 +#define CFG_TEMP_LIMIT_HARD_HOT_SHIFT 4 +#define CFG_TEMP_LIMIT_HARD_COLD_MASK 0xc0 +#define CFG_TEMP_LIMIT_HARD_COLD_SHIFT 6 +#define CFG_FAULT_IRQ 0x0c +#define CFG_FAULT_IRQ_DCIN_UV BIT(2) +#define CFG_STATUS_IRQ 0x0d +#define CFG_STATUS_IRQ_TERMINATION_OR_TAPER BIT(4) +#define CFG_STATUS_IRQ_CHARGE_TIMEOUT BIT(7) +#define CFG_ADDRESS 0x0e + +/* Command registers */ +#define CMD_A 0x30 +#define CMD_A_CHG_ENABLED BIT(1) +#define CMD_A_SUSPEND_ENABLED BIT(2) +#define CMD_A_ALLOW_WRITE BIT(7) +#define CMD_B 0x31 +#define CMD_C 0x33 + +/* Interrupt Status registers */ +#define IRQSTAT_A 0x35 +#define IRQSTAT_C 0x37 +#define IRQSTAT_C_TERMINATION_STAT BIT(0) +#define IRQSTAT_C_TERMINATION_IRQ BIT(1) +#define IRQSTAT_C_TAPER_IRQ BIT(3) +#define IRQSTAT_D 0x38 +#define IRQSTAT_D_CHARGE_TIMEOUT_STAT BIT(2) +#define IRQSTAT_D_CHARGE_TIMEOUT_IRQ BIT(3) +#define IRQSTAT_E 0x39 +#define IRQSTAT_E_USBIN_UV_STAT BIT(0) +#define IRQSTAT_E_USBIN_UV_IRQ BIT(1) +#define IRQSTAT_E_DCIN_UV_STAT BIT(4) +#define IRQSTAT_E_DCIN_UV_IRQ BIT(5) +#define IRQSTAT_F 0x3a + +/* Status registers */ +#define STAT_A 0x3b +#define STAT_A_FLOAT_VOLTAGE_MASK 0x3f +#define STAT_B 0x3c +#define STAT_C 0x3d +#define STAT_C_CHG_ENABLED BIT(0) +#define STAT_C_HOLDOFF_STAT BIT(3) +#define STAT_C_CHG_MASK 0x06 +#define STAT_C_CHG_SHIFT 1 +#define STAT_C_CHG_TERM BIT(5) +#define STAT_C_CHARGER_ERROR BIT(6) +#define STAT_E 0x3f + +#define SMB347_MAX_REGISTER 0x3f + +/** + * struct smb347_charger - smb347 charger instance + * @dev: pointer to device + * @regmap: pointer to driver regmap + * @mains: power_supply instance for AC/DC power + * @usb: power_supply instance for USB power + * @id: SMB charger ID + * @mains_online: is AC/DC input connected + * @usb_online: is USB input connected + * @charging_enabled: is charging enabled + * @irq_unsupported: is interrupt unsupported by SMB hardware + * @max_charge_current: maximum current (in uA) the battery can be charged + * @max_charge_voltage: maximum voltage (in uV) the battery can be charged + * @pre_charge_current: current (in uA) to use in pre-charging phase + * @termination_current: current (in uA) used to determine when the + * charging cycle terminates + * @pre_to_fast_voltage: voltage (in uV) treshold used for transitioning to + * pre-charge to fast charge mode + * @mains_current_limit: maximum input current drawn from AC/DC input (in uA) + * @usb_hc_current_limit: maximum input high current (in uA) drawn from USB + * input + * @chip_temp_threshold: die temperature where device starts limiting charge + * current [%100 - %130] (in degree C) + * @soft_cold_temp_limit: soft cold temperature limit [%0 - %15] (in degree C), + * granularity is 5 deg C. + * @soft_hot_temp_limit: soft hot temperature limit [%40 - %55] (in degree C), + * granularity is 5 deg C. + * @hard_cold_temp_limit: hard cold temperature limit [%-5 - %10] (in degree C), + * granularity is 5 deg C. + * @hard_hot_temp_limit: hard hot temperature limit [%50 - %65] (in degree C), + * granularity is 5 deg C. + * @suspend_on_hard_temp_limit: suspend charging when hard limit is hit + * @soft_temp_limit_compensation: compensation method when soft temperature + * limit is hit + * @charge_current_compensation: current (in uA) for charging compensation + * current when temperature hits soft limits + * @use_mains: AC/DC input can be used + * @use_usb: USB input can be used + * @use_usb_otg: USB OTG output can be used (not implemented yet) + * @enable_control: how charging enable/disable is controlled + * (driver/pin controls) + * + * @use_main, @use_usb, and @use_usb_otg are means to enable/disable + * hardware support for these. This is useful when we want to have for + * example OTG charging controlled via OTG transceiver driver and not by + * the SMB347 hardware. + * + * Hard and soft temperature limit values are given as described in the + * device data sheet and assuming NTC beta value is %3750. Even if this is + * not the case, these values should be used. They can be mapped to the + * corresponding NTC beta values with the help of table %2 in the data + * sheet. So for example if NTC beta is %3375 and we want to program hard + * hot limit to be %53 deg C, @hard_hot_temp_limit should be set to %50. + * + * If zero value is given in any of the current and voltage values, the + * factory programmed default will be used. For soft/hard temperature + * values, pass in %SMB3XX_TEMP_USE_DEFAULT instead. + */ +struct smb347_charger { + struct device *dev; + struct regmap *regmap; + struct power_supply *mains; + struct power_supply *usb; + unsigned int id; + bool mains_online; + bool usb_online; + bool charging_enabled; + bool irq_unsupported; + + unsigned int max_charge_current; + unsigned int max_charge_voltage; + unsigned int pre_charge_current; + unsigned int termination_current; + unsigned int pre_to_fast_voltage; + unsigned int mains_current_limit; + unsigned int usb_hc_current_limit; + unsigned int chip_temp_threshold; + int soft_cold_temp_limit; + int soft_hot_temp_limit; + int hard_cold_temp_limit; + int hard_hot_temp_limit; + bool suspend_on_hard_temp_limit; + unsigned int soft_temp_limit_compensation; + unsigned int charge_current_compensation; + bool use_mains; + bool use_usb; + bool use_usb_otg; + unsigned int enable_control; +}; + +enum smb_charger_chipid { + SMB345, + SMB347, + SMB358, + NUM_CHIP_TYPES, +}; + +/* Fast charge current in uA */ +static const unsigned int fcc_tbl[NUM_CHIP_TYPES][8] = { + [SMB345] = { 200000, 450000, 600000, 900000, + 1300000, 1500000, 1800000, 2000000 }, + [SMB347] = { 700000, 900000, 1200000, 1500000, + 1800000, 2000000, 2200000, 2500000 }, + [SMB358] = { 200000, 450000, 600000, 900000, + 1300000, 1500000, 1800000, 2000000 }, +}; +/* Pre-charge current in uA */ +static const unsigned int pcc_tbl[NUM_CHIP_TYPES][4] = { + [SMB345] = { 150000, 250000, 350000, 450000 }, + [SMB347] = { 100000, 150000, 200000, 250000 }, + [SMB358] = { 150000, 250000, 350000, 450000 }, +}; + +/* Termination current in uA */ +static const unsigned int tc_tbl[NUM_CHIP_TYPES][8] = { + [SMB345] = { 30000, 40000, 60000, 80000, + 100000, 125000, 150000, 200000 }, + [SMB347] = { 37500, 50000, 100000, 150000, + 200000, 250000, 500000, 600000 }, + [SMB358] = { 30000, 40000, 60000, 80000, + 100000, 125000, 150000, 200000 }, +}; + +/* Input current limit in uA */ +static const unsigned int icl_tbl[NUM_CHIP_TYPES][10] = { + [SMB345] = { 300000, 500000, 700000, 1000000, 1500000, + 1800000, 2000000, 2000000, 2000000, 2000000 }, + [SMB347] = { 300000, 500000, 700000, 900000, 1200000, + 1500000, 1800000, 2000000, 2200000, 2500000 }, + [SMB358] = { 300000, 500000, 700000, 1000000, 1500000, + 1800000, 2000000, 2000000, 2000000, 2000000 }, +}; + +/* Charge current compensation in uA */ +static const unsigned int ccc_tbl[NUM_CHIP_TYPES][4] = { + [SMB345] = { 200000, 450000, 600000, 900000 }, + [SMB347] = { 250000, 700000, 900000, 1200000 }, + [SMB358] = { 200000, 450000, 600000, 900000 }, +}; + +/* Convert register value to current using lookup table */ +static int hw_to_current(const unsigned int *tbl, size_t size, unsigned int val) +{ + if (val >= size) + return -EINVAL; + return tbl[val]; +} + +/* Convert current to register value using lookup table */ +static int current_to_hw(const unsigned int *tbl, size_t size, unsigned int val) +{ + size_t i; + + for (i = 0; i < size; i++) + if (val < tbl[i]) + break; + return i > 0 ? i - 1 : -EINVAL; +} + +/** + * smb347_update_ps_status - refreshes the power source status + * @smb: pointer to smb347 charger instance + * + * Function checks whether any power source is connected to the charger and + * updates internal state accordingly. If there is a change to previous state + * function returns %1, otherwise %0 and negative errno in case of errror. + */ +static int smb347_update_ps_status(struct smb347_charger *smb) +{ + bool usb = false; + bool dc = false; + unsigned int val; + int ret; + + ret = regmap_read(smb->regmap, IRQSTAT_E, &val); + if (ret < 0) + return ret; + + /* + * Dc and usb are set depending on whether they are enabled in + * platform data _and_ whether corresponding undervoltage is set. + */ + if (smb->use_mains) + dc = !(val & IRQSTAT_E_DCIN_UV_STAT); + if (smb->use_usb) + usb = !(val & IRQSTAT_E_USBIN_UV_STAT); + + ret = smb->mains_online != dc || smb->usb_online != usb; + smb->mains_online = dc; + smb->usb_online = usb; + + return ret; +} + +/* + * smb347_is_ps_online - returns whether input power source is connected + * @smb: pointer to smb347 charger instance + * + * Returns %true if input power source is connected. Note that this is + * dependent on what platform has configured for usable power sources. For + * example if USB is disabled, this will return %false even if the USB cable + * is connected. + */ +static bool smb347_is_ps_online(struct smb347_charger *smb) +{ + return smb->usb_online || smb->mains_online; +} + +/** + * smb347_charging_status - returns status of charging + * @smb: pointer to smb347 charger instance + * + * Function returns charging status. %0 means no charging is in progress, + * %1 means pre-charging, %2 fast-charging and %3 taper-charging. + */ +static int smb347_charging_status(struct smb347_charger *smb) +{ + unsigned int val; + int ret; + + if (!smb347_is_ps_online(smb)) + return 0; + + ret = regmap_read(smb->regmap, STAT_C, &val); + if (ret < 0) + return 0; + + return (val & STAT_C_CHG_MASK) >> STAT_C_CHG_SHIFT; +} + +static int smb347_charging_set(struct smb347_charger *smb, bool enable) +{ + int ret = 0; + + if (smb->enable_control != SMB3XX_CHG_ENABLE_SW) { + dev_dbg(smb->dev, "charging enable/disable in SW disabled\n"); + return 0; + } + + if (smb->charging_enabled != enable) { + ret = regmap_update_bits(smb->regmap, CMD_A, CMD_A_CHG_ENABLED, + enable ? CMD_A_CHG_ENABLED : 0); + if (!ret) + smb->charging_enabled = enable; + } + + return ret; +} + +static inline int smb347_charging_enable(struct smb347_charger *smb) +{ + return smb347_charging_set(smb, true); +} + +static inline int smb347_charging_disable(struct smb347_charger *smb) +{ + return smb347_charging_set(smb, false); +} + +static int smb347_start_stop_charging(struct smb347_charger *smb) +{ + int ret; + + /* + * Depending on whether valid power source is connected or not, we + * disable or enable the charging. We do it manually because it + * depends on how the platform has configured the valid inputs. + */ + if (smb347_is_ps_online(smb)) { + ret = smb347_charging_enable(smb); + if (ret < 0) + dev_err(smb->dev, "failed to enable charging\n"); + } else { + ret = smb347_charging_disable(smb); + if (ret < 0) + dev_err(smb->dev, "failed to disable charging\n"); + } + + return ret; +} + +static int smb347_set_charge_current(struct smb347_charger *smb) +{ + unsigned int id = smb->id; + int ret; + + if (smb->max_charge_current) { + ret = current_to_hw(fcc_tbl[id], ARRAY_SIZE(fcc_tbl[id]), + smb->max_charge_current); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_CHARGE_CURRENT, + CFG_CHARGE_CURRENT_FCC_MASK, + ret << CFG_CHARGE_CURRENT_FCC_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->pre_charge_current) { + ret = current_to_hw(pcc_tbl[id], ARRAY_SIZE(pcc_tbl[id]), + smb->pre_charge_current); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_CHARGE_CURRENT, + CFG_CHARGE_CURRENT_PCC_MASK, + ret << CFG_CHARGE_CURRENT_PCC_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->termination_current) { + ret = current_to_hw(tc_tbl[id], ARRAY_SIZE(tc_tbl[id]), + smb->termination_current); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_CHARGE_CURRENT, + CFG_CHARGE_CURRENT_TC_MASK, ret); + if (ret < 0) + return ret; + } + + return 0; +} + +static int smb347_set_current_limits(struct smb347_charger *smb) +{ + unsigned int id = smb->id; + int ret; + + if (smb->mains_current_limit) { + ret = current_to_hw(icl_tbl[id], ARRAY_SIZE(icl_tbl[id]), + smb->mains_current_limit); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_CURRENT_LIMIT, + CFG_CURRENT_LIMIT_DC_MASK, + ret << CFG_CURRENT_LIMIT_DC_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->usb_hc_current_limit) { + ret = current_to_hw(icl_tbl[id], ARRAY_SIZE(icl_tbl[id]), + smb->usb_hc_current_limit); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_CURRENT_LIMIT, + CFG_CURRENT_LIMIT_USB_MASK, ret); + if (ret < 0) + return ret; + } + + return 0; +} + +static int smb347_set_voltage_limits(struct smb347_charger *smb) +{ + int ret; + + if (smb->pre_to_fast_voltage) { + ret = smb->pre_to_fast_voltage; + + /* uV */ + ret = clamp_val(ret, 2400000, 3000000) - 2400000; + ret /= 200000; + + ret = regmap_update_bits(smb->regmap, CFG_FLOAT_VOLTAGE, + CFG_FLOAT_VOLTAGE_THRESHOLD_MASK, + ret << CFG_FLOAT_VOLTAGE_THRESHOLD_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->max_charge_voltage) { + ret = smb->max_charge_voltage; + + /* uV */ + ret = clamp_val(ret, 3500000, 4500000) - 3500000; + ret /= 20000; + + ret = regmap_update_bits(smb->regmap, CFG_FLOAT_VOLTAGE, + CFG_FLOAT_VOLTAGE_FLOAT_MASK, ret); + if (ret < 0) + return ret; + } + + return 0; +} + +static int smb347_set_temp_limits(struct smb347_charger *smb) +{ + unsigned int id = smb->id; + bool enable_therm_monitor = false; + int ret = 0; + int val; + + if (smb->chip_temp_threshold) { + val = smb->chip_temp_threshold; + + /* degree C */ + val = clamp_val(val, 100, 130) - 100; + val /= 10; + + ret = regmap_update_bits(smb->regmap, CFG_OTG, + CFG_OTG_TEMP_THRESHOLD_MASK, + val << CFG_OTG_TEMP_THRESHOLD_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->soft_cold_temp_limit != SMB3XX_TEMP_USE_DEFAULT) { + val = smb->soft_cold_temp_limit; + + val = clamp_val(val, 0, 15); + val /= 5; + /* this goes from higher to lower so invert the value */ + val = ~val & 0x3; + + ret = regmap_update_bits(smb->regmap, CFG_TEMP_LIMIT, + CFG_TEMP_LIMIT_SOFT_COLD_MASK, + val << CFG_TEMP_LIMIT_SOFT_COLD_SHIFT); + if (ret < 0) + return ret; + + enable_therm_monitor = true; + } + + if (smb->soft_hot_temp_limit != SMB3XX_TEMP_USE_DEFAULT) { + val = smb->soft_hot_temp_limit; + + val = clamp_val(val, 40, 55) - 40; + val /= 5; + + ret = regmap_update_bits(smb->regmap, CFG_TEMP_LIMIT, + CFG_TEMP_LIMIT_SOFT_HOT_MASK, + val << CFG_TEMP_LIMIT_SOFT_HOT_SHIFT); + if (ret < 0) + return ret; + + enable_therm_monitor = true; + } + + if (smb->hard_cold_temp_limit != SMB3XX_TEMP_USE_DEFAULT) { + val = smb->hard_cold_temp_limit; + + val = clamp_val(val, -5, 10) + 5; + val /= 5; + /* this goes from higher to lower so invert the value */ + val = ~val & 0x3; + + ret = regmap_update_bits(smb->regmap, CFG_TEMP_LIMIT, + CFG_TEMP_LIMIT_HARD_COLD_MASK, + val << CFG_TEMP_LIMIT_HARD_COLD_SHIFT); + if (ret < 0) + return ret; + + enable_therm_monitor = true; + } + + if (smb->hard_hot_temp_limit != SMB3XX_TEMP_USE_DEFAULT) { + val = smb->hard_hot_temp_limit; + + val = clamp_val(val, 50, 65) - 50; + val /= 5; + + ret = regmap_update_bits(smb->regmap, CFG_TEMP_LIMIT, + CFG_TEMP_LIMIT_HARD_HOT_MASK, + val << CFG_TEMP_LIMIT_HARD_HOT_SHIFT); + if (ret < 0) + return ret; + + enable_therm_monitor = true; + } + + /* + * If any of the temperature limits are set, we also enable the + * thermistor monitoring. + * + * When soft limits are hit, the device will start to compensate + * current and/or voltage depending on the configuration. + * + * When hard limit is hit, the device will suspend charging + * depending on the configuration. + */ + if (enable_therm_monitor) { + ret = regmap_update_bits(smb->regmap, CFG_THERM, + CFG_THERM_MONITOR_DISABLED, 0); + if (ret < 0) + return ret; + } + + if (smb->suspend_on_hard_temp_limit) { + ret = regmap_update_bits(smb->regmap, CFG_SYSOK, + CFG_SYSOK_SUSPEND_HARD_LIMIT_DISABLED, 0); + if (ret < 0) + return ret; + } + + if (smb->soft_temp_limit_compensation != + SMB3XX_SOFT_TEMP_COMPENSATE_DEFAULT) { + val = smb->soft_temp_limit_compensation & 0x3; + + ret = regmap_update_bits(smb->regmap, CFG_THERM, + CFG_THERM_SOFT_HOT_COMPENSATION_MASK, + val << CFG_THERM_SOFT_HOT_COMPENSATION_SHIFT); + if (ret < 0) + return ret; + + ret = regmap_update_bits(smb->regmap, CFG_THERM, + CFG_THERM_SOFT_COLD_COMPENSATION_MASK, + val << CFG_THERM_SOFT_COLD_COMPENSATION_SHIFT); + if (ret < 0) + return ret; + } + + if (smb->charge_current_compensation) { + val = current_to_hw(ccc_tbl[id], ARRAY_SIZE(ccc_tbl[id]), + smb->charge_current_compensation); + if (val < 0) + return val; + + ret = regmap_update_bits(smb->regmap, CFG_OTG, + CFG_OTG_CC_COMPENSATION_MASK, + (val & 0x3) << CFG_OTG_CC_COMPENSATION_SHIFT); + if (ret < 0) + return ret; + } + + return ret; +} + +/* + * smb347_set_writable - enables/disables writing to non-volatile registers + * @smb: pointer to smb347 charger instance + * + * You can enable/disable writing to the non-volatile configuration + * registers by calling this function. + * + * Returns %0 on success and negative errno in case of failure. + */ +static int smb347_set_writable(struct smb347_charger *smb, bool writable) +{ + return regmap_update_bits(smb->regmap, CMD_A, CMD_A_ALLOW_WRITE, + writable ? CMD_A_ALLOW_WRITE : 0); +} + +static int smb347_hw_init(struct smb347_charger *smb) +{ + unsigned int val; + int ret; + + ret = smb347_set_writable(smb, true); + if (ret < 0) + return ret; + + /* + * Program the platform specific configuration values to the device + * first. + */ + ret = smb347_set_charge_current(smb); + if (ret < 0) + goto fail; + + ret = smb347_set_current_limits(smb); + if (ret < 0) + goto fail; + + ret = smb347_set_voltage_limits(smb); + if (ret < 0) + goto fail; + + ret = smb347_set_temp_limits(smb); + if (ret < 0) + goto fail; + + /* If USB charging is disabled we put the USB in suspend mode */ + if (!smb->use_usb) { + ret = regmap_update_bits(smb->regmap, CMD_A, + CMD_A_SUSPEND_ENABLED, + CMD_A_SUSPEND_ENABLED); + if (ret < 0) + goto fail; + } + + /* + * If configured by platform data, we enable hardware Auto-OTG + * support for driving VBUS. Otherwise we disable it. + */ + ret = regmap_update_bits(smb->regmap, CFG_OTHER, CFG_OTHER_RID_MASK, + smb->use_usb_otg ? CFG_OTHER_RID_ENABLED_AUTO_OTG : 0); + if (ret < 0) + goto fail; + + /* Activate pin control, making it writable. */ + switch (smb->enable_control) { + case SMB3XX_CHG_ENABLE_PIN_ACTIVE_LOW: + case SMB3XX_CHG_ENABLE_PIN_ACTIVE_HIGH: + ret = regmap_set_bits(smb->regmap, CFG_PIN, CFG_PIN_EN_CTRL); + if (ret < 0) + goto fail; + } + + /* + * Make the charging functionality controllable by a write to the + * command register unless pin control is specified in the platform + * data. + */ + switch (smb->enable_control) { + case SMB3XX_CHG_ENABLE_PIN_ACTIVE_LOW: + val = CFG_PIN_EN_CTRL_ACTIVE_LOW; + break; + case SMB3XX_CHG_ENABLE_PIN_ACTIVE_HIGH: + val = CFG_PIN_EN_CTRL_ACTIVE_HIGH; + break; + default: + val = 0; + break; + } + + ret = regmap_update_bits(smb->regmap, CFG_PIN, CFG_PIN_EN_CTRL_MASK, + val); + if (ret < 0) + goto fail; + + /* Disable Automatic Power Source Detection (APSD) interrupt. */ + ret = regmap_update_bits(smb->regmap, CFG_PIN, CFG_PIN_EN_APSD_IRQ, 0); + if (ret < 0) + goto fail; + + ret = smb347_update_ps_status(smb); + if (ret < 0) + goto fail; + + ret = smb347_start_stop_charging(smb); + +fail: + smb347_set_writable(smb, false); + return ret; +} + +static irqreturn_t smb347_interrupt(int irq, void *data) +{ + struct smb347_charger *smb = data; + unsigned int stat_c, irqstat_c, irqstat_d, irqstat_e; + bool handled = false; + int ret; + + /* SMB347 it needs at least 20ms for setting IRQSTAT_E_*IN_UV_IRQ */ + usleep_range(25000, 35000); + + ret = regmap_read(smb->regmap, STAT_C, &stat_c); + if (ret < 0) { + dev_warn(smb->dev, "reading STAT_C failed\n"); + return IRQ_NONE; + } + + ret = regmap_read(smb->regmap, IRQSTAT_C, &irqstat_c); + if (ret < 0) { + dev_warn(smb->dev, "reading IRQSTAT_C failed\n"); + return IRQ_NONE; + } + + ret = regmap_read(smb->regmap, IRQSTAT_D, &irqstat_d); + if (ret < 0) { + dev_warn(smb->dev, "reading IRQSTAT_D failed\n"); + return IRQ_NONE; + } + + ret = regmap_read(smb->regmap, IRQSTAT_E, &irqstat_e); + if (ret < 0) { + dev_warn(smb->dev, "reading IRQSTAT_E failed\n"); + return IRQ_NONE; + } + + /* + * If we get charger error we report the error back to user. + * If the error is recovered charging will resume again. + */ + if (stat_c & STAT_C_CHARGER_ERROR) { + dev_err(smb->dev, "charging stopped due to charger error\n"); + if (smb->use_mains) + power_supply_changed(smb->mains); + if (smb->use_usb) + power_supply_changed(smb->usb); + handled = true; + } + + /* + * If we reached the termination current the battery is charged and + * we can update the status now. Charging is automatically + * disabled by the hardware. + */ + if (irqstat_c & (IRQSTAT_C_TERMINATION_IRQ | IRQSTAT_C_TAPER_IRQ)) { + if (irqstat_c & IRQSTAT_C_TERMINATION_STAT) { + if (smb->use_mains) + power_supply_changed(smb->mains); + if (smb->use_usb) + power_supply_changed(smb->usb); + } + dev_dbg(smb->dev, "going to HW maintenance mode\n"); + handled = true; + } + + /* + * If we got a charger timeout INT that means the charge + * full is not detected with in charge timeout value. + */ + if (irqstat_d & IRQSTAT_D_CHARGE_TIMEOUT_IRQ) { + dev_dbg(smb->dev, "total Charge Timeout INT received\n"); + + if (irqstat_d & IRQSTAT_D_CHARGE_TIMEOUT_STAT) + dev_warn(smb->dev, "charging stopped due to timeout\n"); + if (smb->use_mains) + power_supply_changed(smb->mains); + if (smb->use_usb) + power_supply_changed(smb->usb); + handled = true; + } + + /* + * If we got an under voltage interrupt it means that AC/USB input + * was connected or disconnected. + */ + if (irqstat_e & (IRQSTAT_E_USBIN_UV_IRQ | IRQSTAT_E_DCIN_UV_IRQ)) { + if (smb347_update_ps_status(smb) > 0) { + smb347_start_stop_charging(smb); + if (smb->use_mains) + power_supply_changed(smb->mains); + if (smb->use_usb) + power_supply_changed(smb->usb); + } + handled = true; + } + + return handled ? IRQ_HANDLED : IRQ_NONE; +} + +static int smb347_irq_set(struct smb347_charger *smb, bool enable) +{ + int ret; + + if (smb->irq_unsupported) + return 0; + + ret = smb347_set_writable(smb, true); + if (ret < 0) + return ret; + + /* + * Enable/disable interrupts for: + * - under voltage + * - termination current reached + * - charger timeout + * - charger error + */ + ret = regmap_update_bits(smb->regmap, CFG_FAULT_IRQ, 0xff, + enable ? CFG_FAULT_IRQ_DCIN_UV : 0); + if (ret < 0) + goto fail; + + ret = regmap_update_bits(smb->regmap, CFG_STATUS_IRQ, 0xff, + enable ? (CFG_STATUS_IRQ_TERMINATION_OR_TAPER | + CFG_STATUS_IRQ_CHARGE_TIMEOUT) : 0); + if (ret < 0) + goto fail; + + ret = regmap_update_bits(smb->regmap, CFG_PIN, CFG_PIN_EN_CHARGER_ERROR, + enable ? CFG_PIN_EN_CHARGER_ERROR : 0); +fail: + smb347_set_writable(smb, false); + return ret; +} + +static inline int smb347_irq_enable(struct smb347_charger *smb) +{ + return smb347_irq_set(smb, true); +} + +static inline int smb347_irq_disable(struct smb347_charger *smb) +{ + return smb347_irq_set(smb, false); +} + +static int smb347_irq_init(struct smb347_charger *smb, + struct i2c_client *client) +{ + int ret; + + ret = devm_request_threaded_irq(smb->dev, client->irq, NULL, + smb347_interrupt, IRQF_ONESHOT, + client->name, smb); + if (ret < 0) + return ret; + + ret = smb347_set_writable(smb, true); + if (ret < 0) + return ret; + + /* + * Configure the STAT output to be suitable for interrupts: disable + * all other output (except interrupts) and make it active low. + */ + ret = regmap_update_bits(smb->regmap, CFG_STAT, + CFG_STAT_ACTIVE_HIGH | CFG_STAT_DISABLED, + CFG_STAT_DISABLED); + + smb347_set_writable(smb, false); + + return ret; +} + +/* + * Returns the constant charge current programmed + * into the charger in uA. + */ +static int get_const_charge_current(struct smb347_charger *smb) +{ + unsigned int id = smb->id; + int ret, intval; + unsigned int v; + + if (!smb347_is_ps_online(smb)) + return -ENODATA; + + ret = regmap_read(smb->regmap, STAT_B, &v); + if (ret < 0) + return ret; + + /* + * The current value is composition of FCC and PCC values + * and we can detect which table to use from bit 5. + */ + if (v & 0x20) { + intval = hw_to_current(fcc_tbl[id], + ARRAY_SIZE(fcc_tbl[id]), v & 7); + } else { + v >>= 3; + intval = hw_to_current(pcc_tbl[id], + ARRAY_SIZE(pcc_tbl[id]), v & 7); + } + + return intval; +} + +/* + * Returns the constant charge voltage programmed + * into the charger in uV. + */ +static int get_const_charge_voltage(struct smb347_charger *smb) +{ + int ret, intval; + unsigned int v; + + if (!smb347_is_ps_online(smb)) + return -ENODATA; + + ret = regmap_read(smb->regmap, STAT_A, &v); + if (ret < 0) + return ret; + + v &= STAT_A_FLOAT_VOLTAGE_MASK; + if (v > 0x3d) + v = 0x3d; + + intval = 3500000 + v * 20000; + + return intval; +} + +static int smb347_get_charging_status(struct smb347_charger *smb, + struct power_supply *psy) +{ + int ret, status; + unsigned int val; + + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) { + if (!smb->usb_online) + return POWER_SUPPLY_STATUS_DISCHARGING; + } else { + if (!smb->mains_online) + return POWER_SUPPLY_STATUS_DISCHARGING; + } + + ret = regmap_read(smb->regmap, STAT_C, &val); + if (ret < 0) + return ret; + + if ((val & STAT_C_CHARGER_ERROR) || + (val & STAT_C_HOLDOFF_STAT)) { + /* + * set to NOT CHARGING upon charger error + * or charging has stopped. + */ + status = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + if ((val & STAT_C_CHG_MASK) >> STAT_C_CHG_SHIFT) { + /* + * set to charging if battery is in pre-charge, + * fast charge or taper charging mode. + */ + status = POWER_SUPPLY_STATUS_CHARGING; + } else if (val & STAT_C_CHG_TERM) { + /* + * set the status to FULL if battery is not in pre + * charge, fast charge or taper charging mode AND + * charging is terminated at least once. + */ + status = POWER_SUPPLY_STATUS_FULL; + } else { + /* + * in this case no charger error or termination + * occured but charging is not in progress!!! + */ + status = POWER_SUPPLY_STATUS_NOT_CHARGING; + } + } + + return status; +} + +static int smb347_get_property_locked(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct smb347_charger *smb = power_supply_get_drvdata(psy); + int ret; + + switch (prop) { + case POWER_SUPPLY_PROP_STATUS: + ret = smb347_get_charging_status(smb, psy); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CHARGE_TYPE: + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) { + if (!smb->usb_online) + return -ENODATA; + } else { + if (!smb->mains_online) + return -ENODATA; + } + + /* + * We handle trickle and pre-charging the same, and taper + * and none the same. + */ + switch (smb347_charging_status(smb)) { + case 1: + val->intval = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case 2: + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + default: + val->intval = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + } + break; + + case POWER_SUPPLY_PROP_ONLINE: + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) + val->intval = smb->usb_online; + else + val->intval = smb->mains_online; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = get_const_charge_voltage(smb); + if (ret < 0) + return ret; + val->intval = ret; + break; + + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + ret = get_const_charge_current(smb); + if (ret < 0) + return ret; + val->intval = ret; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int smb347_get_property(struct power_supply *psy, + enum power_supply_property prop, + union power_supply_propval *val) +{ + struct smb347_charger *smb = power_supply_get_drvdata(psy); + struct i2c_client *client = to_i2c_client(smb->dev); + int ret; + + disable_irq(client->irq); + ret = smb347_get_property_locked(psy, prop, val); + enable_irq(client->irq); + + return ret; +} + +static enum power_supply_property smb347_properties[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, +}; + +static bool smb347_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case IRQSTAT_A: + case IRQSTAT_C: + case IRQSTAT_D: + case IRQSTAT_E: + case IRQSTAT_F: + case STAT_A: + case STAT_B: + case STAT_C: + case STAT_E: + return true; + } + + return false; +} + +static bool smb347_readable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case CFG_CHARGE_CURRENT: + case CFG_CURRENT_LIMIT: + case CFG_FLOAT_VOLTAGE: + case CFG_STAT: + case CFG_PIN: + case CFG_THERM: + case CFG_SYSOK: + case CFG_OTHER: + case CFG_OTG: + case CFG_TEMP_LIMIT: + case CFG_FAULT_IRQ: + case CFG_STATUS_IRQ: + case CFG_ADDRESS: + case CMD_A: + case CMD_B: + case CMD_C: + return true; + } + + return smb347_volatile_reg(dev, reg); +} + +static void smb347_dt_parse_dev_info(struct smb347_charger *smb) +{ + struct device *dev = smb->dev; + + smb->soft_temp_limit_compensation = + SMB3XX_SOFT_TEMP_COMPENSATE_DEFAULT; + /* + * These properties come from the battery info, still we need to + * pre-initialize the values. See smb347_get_battery_info() below. + */ + smb->soft_cold_temp_limit = SMB3XX_TEMP_USE_DEFAULT; + smb->hard_cold_temp_limit = SMB3XX_TEMP_USE_DEFAULT; + smb->soft_hot_temp_limit = SMB3XX_TEMP_USE_DEFAULT; + smb->hard_hot_temp_limit = SMB3XX_TEMP_USE_DEFAULT; + + /* Charging constraints */ + device_property_read_u32(dev, "summit,fast-voltage-threshold-microvolt", + &smb->pre_to_fast_voltage); + device_property_read_u32(dev, "summit,mains-current-limit-microamp", + &smb->mains_current_limit); + device_property_read_u32(dev, "summit,usb-current-limit-microamp", + &smb->usb_hc_current_limit); + + /* For thermometer monitoring */ + device_property_read_u32(dev, "summit,chip-temperature-threshold-celsius", + &smb->chip_temp_threshold); + device_property_read_u32(dev, "summit,soft-compensation-method", + &smb->soft_temp_limit_compensation); + device_property_read_u32(dev, "summit,charge-current-compensation-microamp", + &smb->charge_current_compensation); + + /* Supported charging mode */ + smb->use_mains = device_property_read_bool(dev, "summit,enable-mains-charging"); + smb->use_usb = device_property_read_bool(dev, "summit,enable-usb-charging"); + smb->use_usb_otg = device_property_read_bool(dev, "summit,enable-otg-charging"); + + /* Select charging control */ + device_property_read_u32(dev, "summit,enable-charge-control", + &smb->enable_control); +} + +static int smb347_get_battery_info(struct smb347_charger *smb) +{ + struct power_supply_battery_info info = {}; + struct power_supply *supply; + int err; + + if (smb->mains) + supply = smb->mains; + else + supply = smb->usb; + + err = power_supply_get_battery_info(supply, &info); + if (err == -ENXIO || err == -ENODEV) + return 0; + if (err) + return err; + + if (info.constant_charge_current_max_ua != -EINVAL) + smb->max_charge_current = info.constant_charge_current_max_ua; + + if (info.constant_charge_voltage_max_uv != -EINVAL) + smb->max_charge_voltage = info.constant_charge_voltage_max_uv; + + if (info.precharge_current_ua != -EINVAL) + smb->pre_charge_current = info.precharge_current_ua; + + if (info.charge_term_current_ua != -EINVAL) + smb->termination_current = info.charge_term_current_ua; + + if (info.temp_alert_min != INT_MIN) + smb->soft_cold_temp_limit = info.temp_alert_min; + + if (info.temp_alert_max != INT_MAX) + smb->soft_hot_temp_limit = info.temp_alert_max; + + if (info.temp_min != INT_MIN) + smb->hard_cold_temp_limit = info.temp_min; + + if (info.temp_max != INT_MAX) + smb->hard_hot_temp_limit = info.temp_max; + + /* Suspend when battery temperature is outside hard limits */ + if (smb->hard_cold_temp_limit != SMB3XX_TEMP_USE_DEFAULT || + smb->hard_hot_temp_limit != SMB3XX_TEMP_USE_DEFAULT) + smb->suspend_on_hard_temp_limit = true; + + return 0; +} + +static const struct regmap_config smb347_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = SMB347_MAX_REGISTER, + .volatile_reg = smb347_volatile_reg, + .readable_reg = smb347_readable_reg, +}; + +static const struct power_supply_desc smb347_mains_desc = { + .name = "smb347-mains", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = smb347_get_property, + .properties = smb347_properties, + .num_properties = ARRAY_SIZE(smb347_properties), +}; + +static const struct power_supply_desc smb347_usb_desc = { + .name = "smb347-usb", + .type = POWER_SUPPLY_TYPE_USB, + .get_property = smb347_get_property, + .properties = smb347_properties, + .num_properties = ARRAY_SIZE(smb347_properties), +}; + +static int smb347_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct power_supply_config mains_usb_cfg = {}; + struct device *dev = &client->dev; + struct smb347_charger *smb; + int ret; + + smb = devm_kzalloc(dev, sizeof(*smb), GFP_KERNEL); + if (!smb) + return -ENOMEM; + smb->dev = &client->dev; + smb->id = id->driver_data; + i2c_set_clientdata(client, smb); + + smb347_dt_parse_dev_info(smb); + if (!smb->use_mains && !smb->use_usb) + return -EINVAL; + + smb->regmap = devm_regmap_init_i2c(client, &smb347_regmap); + if (IS_ERR(smb->regmap)) + return PTR_ERR(smb->regmap); + + mains_usb_cfg.drv_data = smb; + mains_usb_cfg.of_node = dev->of_node; + if (smb->use_mains) { + smb->mains = devm_power_supply_register(dev, &smb347_mains_desc, + &mains_usb_cfg); + if (IS_ERR(smb->mains)) + return PTR_ERR(smb->mains); + } + + if (smb->use_usb) { + smb->usb = devm_power_supply_register(dev, &smb347_usb_desc, + &mains_usb_cfg); + if (IS_ERR(smb->usb)) + return PTR_ERR(smb->usb); + } + + ret = smb347_get_battery_info(smb); + if (ret) + return ret; + + ret = smb347_hw_init(smb); + if (ret < 0) + return ret; + + /* + * Interrupt pin is optional. If it is connected, we setup the + * interrupt support here. + */ + if (client->irq) { + ret = smb347_irq_init(smb, client); + if (ret < 0) { + dev_warn(dev, "failed to initialize IRQ: %d\n", ret); + dev_warn(dev, "disabling IRQ support\n"); + smb->irq_unsupported = true; + } else { + smb347_irq_enable(smb); + } + } + + return 0; +} + +static int smb347_remove(struct i2c_client *client) +{ + struct smb347_charger *smb = i2c_get_clientdata(client); + + smb347_irq_disable(smb); + + return 0; +} + +static const struct i2c_device_id smb347_id[] = { + { "smb345", SMB345 }, + { "smb347", SMB347 }, + { "smb358", SMB358 }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, smb347_id); + +static const struct of_device_id smb3xx_of_match[] = { + { .compatible = "summit,smb345" }, + { .compatible = "summit,smb347" }, + { .compatible = "summit,smb358" }, + { }, +}; +MODULE_DEVICE_TABLE(of, smb3xx_of_match); + +static struct i2c_driver smb347_driver = { + .driver = { + .name = "smb347", + .of_match_table = smb3xx_of_match, + }, + .probe = smb347_probe, + .remove = smb347_remove, + .id_table = smb347_id, +}; + +module_i2c_driver(smb347_driver); + +MODULE_AUTHOR("Bruce E. Robertson <bruce.e.robertson@intel.com>"); +MODULE_AUTHOR("Mika Westerberg <mika.westerberg@linux.intel.com>"); +MODULE_DESCRIPTION("SMB347 battery charger driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/test_power.c b/drivers/power/supply/test_power.c new file mode 100644 index 000000000..5f510ddc9 --- /dev/null +++ b/drivers/power/supply/test_power.c @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Power supply driver for testing. + * + * Copyright 2010 Anton Vorontsov <cbouatmailru@gmail.com> + * + * Dynamic module parameter code from the Virtual Battery Driver + * Copyright (C) 2008 Pylone, Inc. + * By: Masashi YOKOTA <yokota@pylone.jp> + * Originally found here: + * http://downloads.pylone.jp/src/virtual_battery/virtual_battery-0.0.1.tar.bz2 + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/power_supply.h> +#include <linux/errno.h> +#include <linux/delay.h> +#include <generated/utsrelease.h> + +enum test_power_id { + TEST_AC, + TEST_BATTERY, + TEST_USB, + TEST_POWER_NUM, +}; + +static int ac_online = 1; +static int usb_online = 1; +static int battery_status = POWER_SUPPLY_STATUS_DISCHARGING; +static int battery_health = POWER_SUPPLY_HEALTH_GOOD; +static int battery_present = 1; /* true */ +static int battery_technology = POWER_SUPPLY_TECHNOLOGY_LION; +static int battery_capacity = 50; +static int battery_voltage = 3300; +static int battery_charge_counter = -1000; +static int battery_current = -1600; + +static bool module_initialized; + +static int test_power_get_ac_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = ac_online; + break; + default: + return -EINVAL; + } + return 0; +} + +static int test_power_get_usb_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = usb_online; + break; + default: + return -EINVAL; + } + return 0; +} + +static int test_power_get_battery_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = "Test battery"; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = "Linux"; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = UTS_RELEASE; + break; + case POWER_SUPPLY_PROP_STATUS: + val->intval = battery_status; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = battery_health; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = battery_present; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = battery_technology; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + break; + case POWER_SUPPLY_PROP_CAPACITY: + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = battery_capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + val->intval = battery_charge_counter; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = 100; + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + val->intval = 3600; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = 26; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = battery_voltage; + break; + case POWER_SUPPLY_PROP_CURRENT_AVG: + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = battery_current; + break; + default: + pr_info("%s: some properties deliberately report errors.\n", + __func__); + return -EINVAL; + } + return 0; +} + +static enum power_supply_property test_power_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static enum power_supply_property test_power_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, +}; + +static char *test_power_ac_supplied_to[] = { + "test_battery", +}; + +static struct power_supply *test_power_supplies[TEST_POWER_NUM]; + +static const struct power_supply_desc test_power_desc[] = { + [TEST_AC] = { + .name = "test_ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = test_power_ac_props, + .num_properties = ARRAY_SIZE(test_power_ac_props), + .get_property = test_power_get_ac_property, + }, + [TEST_BATTERY] = { + .name = "test_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = test_power_battery_props, + .num_properties = ARRAY_SIZE(test_power_battery_props), + .get_property = test_power_get_battery_property, + }, + [TEST_USB] = { + .name = "test_usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = test_power_ac_props, + .num_properties = ARRAY_SIZE(test_power_ac_props), + .get_property = test_power_get_usb_property, + }, +}; + +static const struct power_supply_config test_power_configs[] = { + { + /* test_ac */ + .supplied_to = test_power_ac_supplied_to, + .num_supplicants = ARRAY_SIZE(test_power_ac_supplied_to), + }, { + /* test_battery */ + }, { + /* test_usb */ + .supplied_to = test_power_ac_supplied_to, + .num_supplicants = ARRAY_SIZE(test_power_ac_supplied_to), + }, +}; + +static int __init test_power_init(void) +{ + int i; + int ret; + + BUILD_BUG_ON(TEST_POWER_NUM != ARRAY_SIZE(test_power_supplies)); + BUILD_BUG_ON(TEST_POWER_NUM != ARRAY_SIZE(test_power_configs)); + + for (i = 0; i < ARRAY_SIZE(test_power_supplies); i++) { + test_power_supplies[i] = power_supply_register(NULL, + &test_power_desc[i], + &test_power_configs[i]); + if (IS_ERR(test_power_supplies[i])) { + pr_err("%s: failed to register %s\n", __func__, + test_power_desc[i].name); + ret = PTR_ERR(test_power_supplies[i]); + goto failed; + } + } + + module_initialized = true; + return 0; +failed: + while (--i >= 0) + power_supply_unregister(test_power_supplies[i]); + return ret; +} +module_init(test_power_init); + +static void __exit test_power_exit(void) +{ + int i; + + /* Let's see how we handle changes... */ + ac_online = 0; + usb_online = 0; + battery_status = POWER_SUPPLY_STATUS_DISCHARGING; + for (i = 0; i < ARRAY_SIZE(test_power_supplies); i++) + power_supply_changed(test_power_supplies[i]); + pr_info("%s: 'changed' event sent, sleeping for 10 seconds...\n", + __func__); + ssleep(10); + + for (i = 0; i < ARRAY_SIZE(test_power_supplies); i++) + power_supply_unregister(test_power_supplies[i]); + + module_initialized = false; +} +module_exit(test_power_exit); + + + +#define MAX_KEYLENGTH 256 +struct battery_property_map { + int value; + char const *key; +}; + +static struct battery_property_map map_ac_online[] = { + { 0, "off" }, + { 1, "on" }, + { -1, NULL }, +}; + +static struct battery_property_map map_status[] = { + { POWER_SUPPLY_STATUS_CHARGING, "charging" }, + { POWER_SUPPLY_STATUS_DISCHARGING, "discharging" }, + { POWER_SUPPLY_STATUS_NOT_CHARGING, "not-charging" }, + { POWER_SUPPLY_STATUS_FULL, "full" }, + { -1, NULL }, +}; + +static struct battery_property_map map_health[] = { + { POWER_SUPPLY_HEALTH_GOOD, "good" }, + { POWER_SUPPLY_HEALTH_OVERHEAT, "overheat" }, + { POWER_SUPPLY_HEALTH_DEAD, "dead" }, + { POWER_SUPPLY_HEALTH_OVERVOLTAGE, "overvoltage" }, + { POWER_SUPPLY_HEALTH_UNSPEC_FAILURE, "failure" }, + { -1, NULL }, +}; + +static struct battery_property_map map_present[] = { + { 0, "false" }, + { 1, "true" }, + { -1, NULL }, +}; + +static struct battery_property_map map_technology[] = { + { POWER_SUPPLY_TECHNOLOGY_NiMH, "NiMH" }, + { POWER_SUPPLY_TECHNOLOGY_LION, "LION" }, + { POWER_SUPPLY_TECHNOLOGY_LIPO, "LIPO" }, + { POWER_SUPPLY_TECHNOLOGY_LiFe, "LiFe" }, + { POWER_SUPPLY_TECHNOLOGY_NiCd, "NiCd" }, + { POWER_SUPPLY_TECHNOLOGY_LiMn, "LiMn" }, + { -1, NULL }, +}; + + +static int map_get_value(struct battery_property_map *map, const char *key, + int def_val) +{ + char buf[MAX_KEYLENGTH]; + int cr; + + strncpy(buf, key, MAX_KEYLENGTH); + buf[MAX_KEYLENGTH-1] = '\0'; + + cr = strnlen(buf, MAX_KEYLENGTH) - 1; + if (cr < 0) + return def_val; + if (buf[cr] == '\n') + buf[cr] = '\0'; + + while (map->key) { + if (strncasecmp(map->key, buf, MAX_KEYLENGTH) == 0) + return map->value; + map++; + } + + return def_val; +} + + +static const char *map_get_key(struct battery_property_map *map, int value, + const char *def_key) +{ + while (map->key) { + if (map->value == value) + return map->key; + map++; + } + + return def_key; +} + +static inline void signal_power_supply_changed(struct power_supply *psy) +{ + if (module_initialized) + power_supply_changed(psy); +} + +static int param_set_ac_online(const char *key, const struct kernel_param *kp) +{ + ac_online = map_get_value(map_ac_online, key, ac_online); + signal_power_supply_changed(test_power_supplies[TEST_AC]); + return 0; +} + +static int param_get_ac_online(char *buffer, const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, ac_online, "unknown")); +} + +static int param_set_usb_online(const char *key, const struct kernel_param *kp) +{ + usb_online = map_get_value(map_ac_online, key, usb_online); + signal_power_supply_changed(test_power_supplies[TEST_USB]); + return 0; +} + +static int param_get_usb_online(char *buffer, const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, usb_online, "unknown")); +} + +static int param_set_battery_status(const char *key, + const struct kernel_param *kp) +{ + battery_status = map_get_value(map_status, key, battery_status); + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +static int param_get_battery_status(char *buffer, const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, battery_status, "unknown")); +} + +static int param_set_battery_health(const char *key, + const struct kernel_param *kp) +{ + battery_health = map_get_value(map_health, key, battery_health); + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +static int param_get_battery_health(char *buffer, const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, battery_health, "unknown")); +} + +static int param_set_battery_present(const char *key, + const struct kernel_param *kp) +{ + battery_present = map_get_value(map_present, key, battery_present); + signal_power_supply_changed(test_power_supplies[TEST_AC]); + return 0; +} + +static int param_get_battery_present(char *buffer, + const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, battery_present, "unknown")); +} + +static int param_set_battery_technology(const char *key, + const struct kernel_param *kp) +{ + battery_technology = map_get_value(map_technology, key, + battery_technology); + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +static int param_get_battery_technology(char *buffer, + const struct kernel_param *kp) +{ + return sprintf(buffer, "%s\n", + map_get_key(map_ac_online, battery_technology, + "unknown")); +} + +static int param_set_battery_capacity(const char *key, + const struct kernel_param *kp) +{ + int tmp; + + if (1 != sscanf(key, "%d", &tmp)) + return -EINVAL; + + battery_capacity = tmp; + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +#define param_get_battery_capacity param_get_int + +static int param_set_battery_voltage(const char *key, + const struct kernel_param *kp) +{ + int tmp; + + if (1 != sscanf(key, "%d", &tmp)) + return -EINVAL; + + battery_voltage = tmp; + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +#define param_get_battery_voltage param_get_int + +static int param_set_battery_charge_counter(const char *key, + const struct kernel_param *kp) +{ + int tmp; + + if (1 != sscanf(key, "%d", &tmp)) + return -EINVAL; + + battery_charge_counter = tmp; + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +#define param_get_battery_charge_counter param_get_int + +static int param_set_battery_current(const char *key, + const struct kernel_param *kp) +{ + int tmp; + + if (1 != sscanf(key, "%d", &tmp)) + return -EINVAL; + + battery_current = tmp; + signal_power_supply_changed(test_power_supplies[TEST_BATTERY]); + return 0; +} + +#define param_get_battery_current param_get_int + +static const struct kernel_param_ops param_ops_ac_online = { + .set = param_set_ac_online, + .get = param_get_ac_online, +}; + +static const struct kernel_param_ops param_ops_usb_online = { + .set = param_set_usb_online, + .get = param_get_usb_online, +}; + +static const struct kernel_param_ops param_ops_battery_status = { + .set = param_set_battery_status, + .get = param_get_battery_status, +}; + +static const struct kernel_param_ops param_ops_battery_present = { + .set = param_set_battery_present, + .get = param_get_battery_present, +}; + +static const struct kernel_param_ops param_ops_battery_technology = { + .set = param_set_battery_technology, + .get = param_get_battery_technology, +}; + +static const struct kernel_param_ops param_ops_battery_health = { + .set = param_set_battery_health, + .get = param_get_battery_health, +}; + +static const struct kernel_param_ops param_ops_battery_capacity = { + .set = param_set_battery_capacity, + .get = param_get_battery_capacity, +}; + +static const struct kernel_param_ops param_ops_battery_voltage = { + .set = param_set_battery_voltage, + .get = param_get_battery_voltage, +}; + +static const struct kernel_param_ops param_ops_battery_charge_counter = { + .set = param_set_battery_charge_counter, + .get = param_get_battery_charge_counter, +}; + +static const struct kernel_param_ops param_ops_battery_current = { + .set = param_set_battery_current, + .get = param_get_battery_current, +}; + +#define param_check_ac_online(name, p) __param_check(name, p, void); +#define param_check_usb_online(name, p) __param_check(name, p, void); +#define param_check_battery_status(name, p) __param_check(name, p, void); +#define param_check_battery_present(name, p) __param_check(name, p, void); +#define param_check_battery_technology(name, p) __param_check(name, p, void); +#define param_check_battery_health(name, p) __param_check(name, p, void); +#define param_check_battery_capacity(name, p) __param_check(name, p, void); +#define param_check_battery_voltage(name, p) __param_check(name, p, void); +#define param_check_battery_charge_counter(name, p) __param_check(name, p, void); +#define param_check_battery_current(name, p) __param_check(name, p, void); + + +module_param(ac_online, ac_online, 0644); +MODULE_PARM_DESC(ac_online, "AC charging state <on|off>"); + +module_param(usb_online, usb_online, 0644); +MODULE_PARM_DESC(usb_online, "USB charging state <on|off>"); + +module_param(battery_status, battery_status, 0644); +MODULE_PARM_DESC(battery_status, + "battery status <charging|discharging|not-charging|full>"); + +module_param(battery_present, battery_present, 0644); +MODULE_PARM_DESC(battery_present, + "battery presence state <good|overheat|dead|overvoltage|failure>"); + +module_param(battery_technology, battery_technology, 0644); +MODULE_PARM_DESC(battery_technology, + "battery technology <NiMH|LION|LIPO|LiFe|NiCd|LiMn>"); + +module_param(battery_health, battery_health, 0644); +MODULE_PARM_DESC(battery_health, + "battery health state <good|overheat|dead|overvoltage|failure>"); + +module_param(battery_capacity, battery_capacity, 0644); +MODULE_PARM_DESC(battery_capacity, "battery capacity (percentage)"); + +module_param(battery_voltage, battery_voltage, 0644); +MODULE_PARM_DESC(battery_voltage, "battery voltage (millivolts)"); + +module_param(battery_charge_counter, battery_charge_counter, 0644); +MODULE_PARM_DESC(battery_charge_counter, + "battery charge counter (microampere-hours)"); + +module_param(battery_current, battery_current, 0644); +MODULE_PARM_DESC(battery_current, "battery current (milliampere)"); + +MODULE_DESCRIPTION("Power supply driver for testing"); +MODULE_AUTHOR("Anton Vorontsov <cbouatmailru@gmail.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/tosa_battery.c b/drivers/power/supply/tosa_battery.c new file mode 100644 index 000000000..32cc31cd4 --- /dev/null +++ b/drivers/power/supply/tosa_battery.c @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery and Power Management code for the Sharp SL-6000x + * + * Copyright (c) 2005 Dirk Opfer + * Copyright (c) 2008 Dmitry Baryshkov + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/power_supply.h> +#include <linux/wm97xx.h> +#include <linux/delay.h> +#include <linux/spinlock.h> +#include <linux/interrupt.h> +#include <linux/gpio.h> + +#include <asm/mach-types.h> +#include <mach/tosa.h> + +static DEFINE_MUTEX(bat_lock); /* protects gpio pins */ +static struct work_struct bat_work; + +struct tosa_bat { + int status; + struct power_supply *psy; + int full_chrg; + + struct mutex work_lock; /* protects data */ + + bool (*is_present)(struct tosa_bat *bat); + int gpio_full; + int gpio_charge_off; + + int technology; + + int gpio_bat; + int adc_bat; + int adc_bat_divider; + int bat_max; + int bat_min; + + int gpio_temp; + int adc_temp; + int adc_temp_divider; +}; + +static struct tosa_bat tosa_bat_main; +static struct tosa_bat tosa_bat_jacket; + +static unsigned long tosa_read_bat(struct tosa_bat *bat) +{ + unsigned long value = 0; + + if (bat->gpio_bat < 0 || bat->adc_bat < 0) + return 0; + + mutex_lock(&bat_lock); + gpio_set_value(bat->gpio_bat, 1); + msleep(5); + value = wm97xx_read_aux_adc(dev_get_drvdata(bat->psy->dev.parent), + bat->adc_bat); + gpio_set_value(bat->gpio_bat, 0); + mutex_unlock(&bat_lock); + + value = value * 1000000 / bat->adc_bat_divider; + + return value; +} + +static unsigned long tosa_read_temp(struct tosa_bat *bat) +{ + unsigned long value = 0; + + if (bat->gpio_temp < 0 || bat->adc_temp < 0) + return 0; + + mutex_lock(&bat_lock); + gpio_set_value(bat->gpio_temp, 1); + msleep(5); + value = wm97xx_read_aux_adc(dev_get_drvdata(bat->psy->dev.parent), + bat->adc_temp); + gpio_set_value(bat->gpio_temp, 0); + mutex_unlock(&bat_lock); + + value = value * 10000 / bat->adc_temp_divider; + + return value; +} + +static int tosa_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + int ret = 0; + struct tosa_bat *bat = power_supply_get_drvdata(psy); + + if (bat->is_present && !bat->is_present(bat) + && psp != POWER_SUPPLY_PROP_PRESENT) { + return -ENODEV; + } + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = bat->status; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = bat->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = tosa_read_bat(bat); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + if (bat->full_chrg == -1) + val->intval = bat->bat_max; + else + val->intval = bat->full_chrg; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = bat->bat_max; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = bat->bat_min; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = tosa_read_temp(bat); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = bat->is_present ? bat->is_present(bat) : 1; + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static bool tosa_jacket_bat_is_present(struct tosa_bat *bat) +{ + return gpio_get_value(TOSA_GPIO_JACKET_DETECT) == 0; +} + +static void tosa_bat_external_power_changed(struct power_supply *psy) +{ + schedule_work(&bat_work); +} + +static irqreturn_t tosa_bat_gpio_isr(int irq, void *data) +{ + pr_info("tosa_bat_gpio irq\n"); + schedule_work(&bat_work); + return IRQ_HANDLED; +} + +static void tosa_bat_update(struct tosa_bat *bat) +{ + int old; + struct power_supply *psy = bat->psy; + + mutex_lock(&bat->work_lock); + + old = bat->status; + + if (bat->is_present && !bat->is_present(bat)) { + printk(KERN_NOTICE "%s not present\n", psy->desc->name); + bat->status = POWER_SUPPLY_STATUS_UNKNOWN; + bat->full_chrg = -1; + } else if (power_supply_am_i_supplied(psy)) { + if (bat->status == POWER_SUPPLY_STATUS_DISCHARGING) { + gpio_set_value(bat->gpio_charge_off, 0); + mdelay(15); + } + + if (gpio_get_value(bat->gpio_full)) { + if (old == POWER_SUPPLY_STATUS_CHARGING || + bat->full_chrg == -1) + bat->full_chrg = tosa_read_bat(bat); + + gpio_set_value(bat->gpio_charge_off, 1); + bat->status = POWER_SUPPLY_STATUS_FULL; + } else { + gpio_set_value(bat->gpio_charge_off, 0); + bat->status = POWER_SUPPLY_STATUS_CHARGING; + } + } else { + gpio_set_value(bat->gpio_charge_off, 1); + bat->status = POWER_SUPPLY_STATUS_DISCHARGING; + } + + if (old != bat->status) + power_supply_changed(psy); + + mutex_unlock(&bat->work_lock); +} + +static void tosa_bat_work(struct work_struct *work) +{ + tosa_bat_update(&tosa_bat_main); + tosa_bat_update(&tosa_bat_jacket); +} + + +static enum power_supply_property tosa_bat_main_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_PRESENT, +}; + +static enum power_supply_property tosa_bat_bu_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_PRESENT, +}; + +static const struct power_supply_desc tosa_bat_main_desc = { + .name = "main-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = tosa_bat_main_props, + .num_properties = ARRAY_SIZE(tosa_bat_main_props), + .get_property = tosa_bat_get_property, + .external_power_changed = tosa_bat_external_power_changed, + .use_for_apm = 1, +}; + +static const struct power_supply_desc tosa_bat_jacket_desc = { + .name = "jacket-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = tosa_bat_main_props, + .num_properties = ARRAY_SIZE(tosa_bat_main_props), + .get_property = tosa_bat_get_property, + .external_power_changed = tosa_bat_external_power_changed, +}; + +static const struct power_supply_desc tosa_bat_bu_desc = { + .name = "backup-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = tosa_bat_bu_props, + .num_properties = ARRAY_SIZE(tosa_bat_bu_props), + .get_property = tosa_bat_get_property, + .external_power_changed = tosa_bat_external_power_changed, +}; + +static struct tosa_bat tosa_bat_main = { + .status = POWER_SUPPLY_STATUS_DISCHARGING, + .full_chrg = -1, + .psy = NULL, + + .gpio_full = TOSA_GPIO_BAT0_CRG, + .gpio_charge_off = TOSA_GPIO_CHARGE_OFF, + + .technology = POWER_SUPPLY_TECHNOLOGY_LIPO, + + .gpio_bat = TOSA_GPIO_BAT0_V_ON, + .adc_bat = WM97XX_AUX_ID3, + .adc_bat_divider = 414, + .bat_max = 4310000, + .bat_min = 1551 * 1000000 / 414, + + .gpio_temp = TOSA_GPIO_BAT1_TH_ON, + .adc_temp = WM97XX_AUX_ID2, + .adc_temp_divider = 10000, +}; + +static struct tosa_bat tosa_bat_jacket = { + .status = POWER_SUPPLY_STATUS_DISCHARGING, + .full_chrg = -1, + .psy = NULL, + + .is_present = tosa_jacket_bat_is_present, + .gpio_full = TOSA_GPIO_BAT1_CRG, + .gpio_charge_off = TOSA_GPIO_CHARGE_OFF_JC, + + .technology = POWER_SUPPLY_TECHNOLOGY_LIPO, + + .gpio_bat = TOSA_GPIO_BAT1_V_ON, + .adc_bat = WM97XX_AUX_ID3, + .adc_bat_divider = 414, + .bat_max = 4310000, + .bat_min = 1551 * 1000000 / 414, + + .gpio_temp = TOSA_GPIO_BAT0_TH_ON, + .adc_temp = WM97XX_AUX_ID2, + .adc_temp_divider = 10000, +}; + +static struct tosa_bat tosa_bat_bu = { + .status = POWER_SUPPLY_STATUS_UNKNOWN, + .full_chrg = -1, + .psy = NULL, + + .gpio_full = -1, + .gpio_charge_off = -1, + + .technology = POWER_SUPPLY_TECHNOLOGY_LiMn, + + .gpio_bat = TOSA_GPIO_BU_CHRG_ON, + .adc_bat = WM97XX_AUX_ID4, + .adc_bat_divider = 1266, + + .gpio_temp = -1, + .adc_temp = -1, + .adc_temp_divider = -1, +}; + +static struct gpio tosa_bat_gpios[] = { + { TOSA_GPIO_CHARGE_OFF, GPIOF_OUT_INIT_HIGH, "main charge off" }, + { TOSA_GPIO_CHARGE_OFF_JC, GPIOF_OUT_INIT_HIGH, "jacket charge off" }, + { TOSA_GPIO_BAT_SW_ON, GPIOF_OUT_INIT_LOW, "battery switch" }, + { TOSA_GPIO_BAT0_V_ON, GPIOF_OUT_INIT_LOW, "main battery" }, + { TOSA_GPIO_BAT1_V_ON, GPIOF_OUT_INIT_LOW, "jacket battery" }, + { TOSA_GPIO_BAT1_TH_ON, GPIOF_OUT_INIT_LOW, "main battery temp" }, + { TOSA_GPIO_BAT0_TH_ON, GPIOF_OUT_INIT_LOW, "jacket battery temp" }, + { TOSA_GPIO_BU_CHRG_ON, GPIOF_OUT_INIT_LOW, "backup battery" }, + { TOSA_GPIO_BAT0_CRG, GPIOF_IN, "main battery full" }, + { TOSA_GPIO_BAT1_CRG, GPIOF_IN, "jacket battery full" }, + { TOSA_GPIO_BAT0_LOW, GPIOF_IN, "main battery low" }, + { TOSA_GPIO_BAT1_LOW, GPIOF_IN, "jacket battery low" }, + { TOSA_GPIO_JACKET_DETECT, GPIOF_IN, "jacket detect" }, +}; + +#ifdef CONFIG_PM +static int tosa_bat_suspend(struct platform_device *dev, pm_message_t state) +{ + /* flush all pending status updates */ + flush_work(&bat_work); + return 0; +} + +static int tosa_bat_resume(struct platform_device *dev) +{ + /* things may have changed while we were away */ + schedule_work(&bat_work); + return 0; +} +#else +#define tosa_bat_suspend NULL +#define tosa_bat_resume NULL +#endif + +static int tosa_bat_probe(struct platform_device *dev) +{ + int ret; + struct power_supply_config main_psy_cfg = {}, + jacket_psy_cfg = {}, + bu_psy_cfg = {}; + + if (!machine_is_tosa()) + return -ENODEV; + + ret = gpio_request_array(tosa_bat_gpios, ARRAY_SIZE(tosa_bat_gpios)); + if (ret) + return ret; + + mutex_init(&tosa_bat_main.work_lock); + mutex_init(&tosa_bat_jacket.work_lock); + + INIT_WORK(&bat_work, tosa_bat_work); + + main_psy_cfg.drv_data = &tosa_bat_main; + tosa_bat_main.psy = power_supply_register(&dev->dev, + &tosa_bat_main_desc, + &main_psy_cfg); + if (IS_ERR(tosa_bat_main.psy)) { + ret = PTR_ERR(tosa_bat_main.psy); + goto err_psy_reg_main; + } + + jacket_psy_cfg.drv_data = &tosa_bat_jacket; + tosa_bat_jacket.psy = power_supply_register(&dev->dev, + &tosa_bat_jacket_desc, + &jacket_psy_cfg); + if (IS_ERR(tosa_bat_jacket.psy)) { + ret = PTR_ERR(tosa_bat_jacket.psy); + goto err_psy_reg_jacket; + } + + bu_psy_cfg.drv_data = &tosa_bat_bu; + tosa_bat_bu.psy = power_supply_register(&dev->dev, &tosa_bat_bu_desc, + &bu_psy_cfg); + if (IS_ERR(tosa_bat_bu.psy)) { + ret = PTR_ERR(tosa_bat_bu.psy); + goto err_psy_reg_bu; + } + + ret = request_irq(gpio_to_irq(TOSA_GPIO_BAT0_CRG), + tosa_bat_gpio_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "main full", &tosa_bat_main); + if (ret) + goto err_req_main; + + ret = request_irq(gpio_to_irq(TOSA_GPIO_BAT1_CRG), + tosa_bat_gpio_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "jacket full", &tosa_bat_jacket); + if (ret) + goto err_req_jacket; + + ret = request_irq(gpio_to_irq(TOSA_GPIO_JACKET_DETECT), + tosa_bat_gpio_isr, + IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, + "jacket detect", &tosa_bat_jacket); + if (!ret) { + schedule_work(&bat_work); + return 0; + } + + free_irq(gpio_to_irq(TOSA_GPIO_BAT1_CRG), &tosa_bat_jacket); +err_req_jacket: + free_irq(gpio_to_irq(TOSA_GPIO_BAT0_CRG), &tosa_bat_main); +err_req_main: + power_supply_unregister(tosa_bat_bu.psy); +err_psy_reg_bu: + power_supply_unregister(tosa_bat_jacket.psy); +err_psy_reg_jacket: + power_supply_unregister(tosa_bat_main.psy); +err_psy_reg_main: + + /* see comment in tosa_bat_remove */ + cancel_work_sync(&bat_work); + + gpio_free_array(tosa_bat_gpios, ARRAY_SIZE(tosa_bat_gpios)); + return ret; +} + +static int tosa_bat_remove(struct platform_device *dev) +{ + free_irq(gpio_to_irq(TOSA_GPIO_JACKET_DETECT), &tosa_bat_jacket); + free_irq(gpio_to_irq(TOSA_GPIO_BAT1_CRG), &tosa_bat_jacket); + free_irq(gpio_to_irq(TOSA_GPIO_BAT0_CRG), &tosa_bat_main); + + power_supply_unregister(tosa_bat_bu.psy); + power_supply_unregister(tosa_bat_jacket.psy); + power_supply_unregister(tosa_bat_main.psy); + + /* + * Now cancel the bat_work. We won't get any more schedules, + * since all sources (isr and external_power_changed) are + * unregistered now. + */ + cancel_work_sync(&bat_work); + gpio_free_array(tosa_bat_gpios, ARRAY_SIZE(tosa_bat_gpios)); + return 0; +} + +static struct platform_driver tosa_bat_driver = { + .driver.name = "wm97xx-battery", + .driver.owner = THIS_MODULE, + .probe = tosa_bat_probe, + .remove = tosa_bat_remove, + .suspend = tosa_bat_suspend, + .resume = tosa_bat_resume, +}; + +module_platform_driver(tosa_bat_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Dmitry Baryshkov"); +MODULE_DESCRIPTION("Tosa battery driver"); +MODULE_ALIAS("platform:wm97xx-battery"); diff --git a/drivers/power/supply/tps65090-charger.c b/drivers/power/supply/tps65090-charger.c new file mode 100644 index 000000000..0990b2fa6 --- /dev/null +++ b/drivers/power/supply/tps65090-charger.c @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery charger driver for TI's tps65090 + * + * Copyright (c) 2013, NVIDIA CORPORATION. All rights reserved. + + */ +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/freezer.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/kthread.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#include <linux/mfd/tps65090.h> + +#define TPS65090_CHARGER_ENABLE BIT(0) +#define TPS65090_VACG BIT(1) +#define TPS65090_NOITERM BIT(5) + +#define POLL_INTERVAL (HZ * 2) /* Used when no irq */ + +struct tps65090_charger { + struct device *dev; + int ac_online; + int prev_ac_online; + int irq; + struct task_struct *poll_task; + bool passive_mode; + struct power_supply *ac; + struct tps65090_platform_data *pdata; +}; + +static enum power_supply_property tps65090_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static int tps65090_low_chrg_current(struct tps65090_charger *charger) +{ + int ret; + + if (charger->passive_mode) + return 0; + + ret = tps65090_write(charger->dev->parent, TPS65090_REG_CG_CTRL5, + TPS65090_NOITERM); + if (ret < 0) { + dev_err(charger->dev, "%s(): error reading in register 0x%x\n", + __func__, TPS65090_REG_CG_CTRL5); + return ret; + } + return 0; +} + +static int tps65090_enable_charging(struct tps65090_charger *charger) +{ + int ret; + uint8_t ctrl0 = 0; + + if (charger->passive_mode) + return 0; + + ret = tps65090_read(charger->dev->parent, TPS65090_REG_CG_CTRL0, + &ctrl0); + if (ret < 0) { + dev_err(charger->dev, "%s(): error reading in register 0x%x\n", + __func__, TPS65090_REG_CG_CTRL0); + return ret; + } + + ret = tps65090_write(charger->dev->parent, TPS65090_REG_CG_CTRL0, + (ctrl0 | TPS65090_CHARGER_ENABLE)); + if (ret < 0) { + dev_err(charger->dev, "%s(): error writing in register 0x%x\n", + __func__, TPS65090_REG_CG_CTRL0); + return ret; + } + return 0; +} + +static int tps65090_config_charger(struct tps65090_charger *charger) +{ + uint8_t intrmask = 0; + int ret; + + if (charger->passive_mode) + return 0; + + if (charger->pdata->enable_low_current_chrg) { + ret = tps65090_low_chrg_current(charger); + if (ret < 0) { + dev_err(charger->dev, + "error configuring low charge current\n"); + return ret; + } + } + + /* Enable the VACG interrupt for AC power detect */ + ret = tps65090_read(charger->dev->parent, TPS65090_REG_INTR_MASK, + &intrmask); + if (ret < 0) { + dev_err(charger->dev, "%s(): error reading in register 0x%x\n", + __func__, TPS65090_REG_INTR_MASK); + return ret; + } + + ret = tps65090_write(charger->dev->parent, TPS65090_REG_INTR_MASK, + (intrmask | TPS65090_VACG)); + if (ret < 0) { + dev_err(charger->dev, "%s(): error writing in register 0x%x\n", + __func__, TPS65090_REG_CG_CTRL0); + return ret; + } + + return 0; +} + +static int tps65090_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct tps65090_charger *charger = power_supply_get_drvdata(psy); + + if (psp == POWER_SUPPLY_PROP_ONLINE) { + val->intval = charger->ac_online; + charger->prev_ac_online = charger->ac_online; + return 0; + } + return -EINVAL; +} + +static irqreturn_t tps65090_charger_isr(int irq, void *dev_id) +{ + struct tps65090_charger *charger = dev_id; + int ret; + uint8_t status1 = 0; + uint8_t intrsts = 0; + + ret = tps65090_read(charger->dev->parent, TPS65090_REG_CG_STATUS1, + &status1); + if (ret < 0) { + dev_err(charger->dev, "%s(): Error in reading reg 0x%x\n", + __func__, TPS65090_REG_CG_STATUS1); + return IRQ_HANDLED; + } + msleep(75); + ret = tps65090_read(charger->dev->parent, TPS65090_REG_INTR_STS, + &intrsts); + if (ret < 0) { + dev_err(charger->dev, "%s(): Error in reading reg 0x%x\n", + __func__, TPS65090_REG_INTR_STS); + return IRQ_HANDLED; + } + + if (intrsts & TPS65090_VACG) { + ret = tps65090_enable_charging(charger); + if (ret < 0) + return IRQ_HANDLED; + charger->ac_online = 1; + } else { + charger->ac_online = 0; + } + + /* Clear interrupts. */ + if (!charger->passive_mode) { + ret = tps65090_write(charger->dev->parent, + TPS65090_REG_INTR_STS, 0x00); + if (ret < 0) { + dev_err(charger->dev, + "%s(): Error in writing reg 0x%x\n", + __func__, TPS65090_REG_INTR_STS); + } + } + + if (charger->prev_ac_online != charger->ac_online) + power_supply_changed(charger->ac); + + return IRQ_HANDLED; +} + +static struct tps65090_platform_data * + tps65090_parse_dt_charger_data(struct platform_device *pdev) +{ + struct tps65090_platform_data *pdata; + struct device_node *np = pdev->dev.of_node; + unsigned int prop; + + pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) { + dev_err(&pdev->dev, "Memory alloc for tps65090_pdata failed\n"); + return NULL; + } + + prop = of_property_read_bool(np, "ti,enable-low-current-chrg"); + pdata->enable_low_current_chrg = prop; + + pdata->irq_base = -1; + + return pdata; + +} + +static int tps65090_charger_poll_task(void *data) +{ + set_freezable(); + + while (!kthread_should_stop()) { + schedule_timeout_interruptible(POLL_INTERVAL); + try_to_freeze(); + tps65090_charger_isr(-1, data); + } + return 0; +} + +static const struct power_supply_desc tps65090_charger_desc = { + .name = "tps65090-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = tps65090_ac_get_property, + .properties = tps65090_ac_props, + .num_properties = ARRAY_SIZE(tps65090_ac_props), +}; + +static int tps65090_charger_probe(struct platform_device *pdev) +{ + struct tps65090_charger *cdata; + struct tps65090_platform_data *pdata; + struct power_supply_config psy_cfg = {}; + uint8_t status1 = 0; + int ret; + int irq; + + pdata = dev_get_platdata(pdev->dev.parent); + + if (IS_ENABLED(CONFIG_OF) && !pdata && pdev->dev.of_node) + pdata = tps65090_parse_dt_charger_data(pdev); + + if (!pdata) { + dev_err(&pdev->dev, "%s():no platform data available\n", + __func__); + return -ENODEV; + } + + cdata = devm_kzalloc(&pdev->dev, sizeof(*cdata), GFP_KERNEL); + if (!cdata) { + dev_err(&pdev->dev, "failed to allocate memory status\n"); + return -ENOMEM; + } + + platform_set_drvdata(pdev, cdata); + + cdata->dev = &pdev->dev; + cdata->pdata = pdata; + + psy_cfg.supplied_to = pdata->supplied_to; + psy_cfg.num_supplicants = pdata->num_supplicants; + psy_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = cdata; + + cdata->ac = power_supply_register(&pdev->dev, &tps65090_charger_desc, + &psy_cfg); + if (IS_ERR(cdata->ac)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + return PTR_ERR(cdata->ac); + } + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + irq = -ENXIO; + cdata->irq = irq; + + ret = tps65090_config_charger(cdata); + if (ret < 0) { + dev_err(&pdev->dev, "charger config failed, err %d\n", ret); + goto fail_unregister_supply; + } + + /* Check for charger presence */ + ret = tps65090_read(cdata->dev->parent, TPS65090_REG_CG_STATUS1, + &status1); + if (ret < 0) { + dev_err(cdata->dev, "%s(): Error in reading reg 0x%x", __func__, + TPS65090_REG_CG_STATUS1); + goto fail_unregister_supply; + } + + if (status1 != 0) { + ret = tps65090_enable_charging(cdata); + if (ret < 0) { + dev_err(cdata->dev, "error enabling charger\n"); + goto fail_unregister_supply; + } + cdata->ac_online = 1; + power_supply_changed(cdata->ac); + } + + if (irq != -ENXIO) { + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + tps65090_charger_isr, IRQF_ONESHOT, "tps65090-charger", cdata); + if (ret) { + dev_err(cdata->dev, + "Unable to register irq %d err %d\n", irq, + ret); + goto fail_unregister_supply; + } + } else { + cdata->poll_task = kthread_run(tps65090_charger_poll_task, + cdata, "ktps65090charger"); + cdata->passive_mode = true; + if (IS_ERR(cdata->poll_task)) { + ret = PTR_ERR(cdata->poll_task); + dev_err(cdata->dev, + "Unable to run kthread err %d\n", ret); + goto fail_unregister_supply; + } + } + + return 0; + +fail_unregister_supply: + power_supply_unregister(cdata->ac); + + return ret; +} + +static int tps65090_charger_remove(struct platform_device *pdev) +{ + struct tps65090_charger *cdata = platform_get_drvdata(pdev); + + if (cdata->irq == -ENXIO) + kthread_stop(cdata->poll_task); + power_supply_unregister(cdata->ac); + + return 0; +} + +static const struct of_device_id of_tps65090_charger_match[] = { + { .compatible = "ti,tps65090-charger", }, + { /* end */ } +}; +MODULE_DEVICE_TABLE(of, of_tps65090_charger_match); + +static struct platform_driver tps65090_charger_driver = { + .driver = { + .name = "tps65090-charger", + .of_match_table = of_tps65090_charger_match, + }, + .probe = tps65090_charger_probe, + .remove = tps65090_charger_remove, +}; +module_platform_driver(tps65090_charger_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Syed Rafiuddin <srafiuddin@nvidia.com>"); +MODULE_DESCRIPTION("tps65090 battery charger driver"); diff --git a/drivers/power/supply/tps65217_charger.c b/drivers/power/supply/tps65217_charger.c new file mode 100644 index 000000000..ba33d1617 --- /dev/null +++ b/drivers/power/supply/tps65217_charger.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0 +// Battery charger driver for TI's tps65217 +// +// Copyright (C) 2015 Collabora Ltd. +// Author: Enric Balletbo i Serra <enric.balletbo@collabora.com> + +/* + * Battery charger driver for TI's tps65217 + */ +#include <linux/kernel.h> +#include <linux/kthread.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/power_supply.h> + +#include <linux/mfd/core.h> +#include <linux/mfd/tps65217.h> + +#define CHARGER_STATUS_PRESENT (TPS65217_STATUS_ACPWR | TPS65217_STATUS_USBPWR) +#define NUM_CHARGER_IRQS 2 +#define POLL_INTERVAL (HZ * 2) + +struct tps65217_charger { + struct tps65217 *tps; + struct device *dev; + struct power_supply *psy; + + int online; + int prev_online; + + struct task_struct *poll_task; +}; + +static enum power_supply_property tps65217_charger_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static int tps65217_config_charger(struct tps65217_charger *charger) +{ + int ret; + + /* + * tps65217 rev. G, p. 31 (see p. 32 for NTC schematic) + * + * The device can be configured to support a 100k NTC (B = 3960) by + * setting the the NTC_TYPE bit in register CHGCONFIG1 to 1. However it + * is not recommended to do so. In sleep mode, the charger continues + * charging the battery, but all register values are reset to default + * values. Therefore, the charger would get the wrong temperature + * information. If 100k NTC setting is required, please contact the + * factory. + * + * ATTENTION, conflicting information, from p. 46 + * + * NTC TYPE (for battery temperature measurement) + * 0 – 100k (curve 1, B = 3960) + * 1 – 10k (curve 2, B = 3480) (default on reset) + * + */ + ret = tps65217_clear_bits(charger->tps, TPS65217_REG_CHGCONFIG1, + TPS65217_CHGCONFIG1_NTC_TYPE, + TPS65217_PROTECT_NONE); + if (ret) { + dev_err(charger->dev, + "failed to set 100k NTC setting: %d\n", ret); + return ret; + } + + return 0; +} + +static int tps65217_enable_charging(struct tps65217_charger *charger) +{ + int ret; + + /* charger already enabled */ + if (charger->online) + return 0; + + dev_dbg(charger->dev, "%s: enable charging\n", __func__); + ret = tps65217_set_bits(charger->tps, TPS65217_REG_CHGCONFIG1, + TPS65217_CHGCONFIG1_CHG_EN, + TPS65217_CHGCONFIG1_CHG_EN, + TPS65217_PROTECT_NONE); + if (ret) { + dev_err(charger->dev, + "%s: Error in writing CHG_EN in reg 0x%x: %d\n", + __func__, TPS65217_REG_CHGCONFIG1, ret); + return ret; + } + + charger->online = 1; + + return 0; +} + +static int tps65217_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct tps65217_charger *charger = power_supply_get_drvdata(psy); + + if (psp == POWER_SUPPLY_PROP_ONLINE) { + val->intval = charger->online; + return 0; + } + return -EINVAL; +} + +static irqreturn_t tps65217_charger_irq(int irq, void *dev) +{ + int ret, val; + struct tps65217_charger *charger = dev; + + charger->prev_online = charger->online; + + ret = tps65217_reg_read(charger->tps, TPS65217_REG_STATUS, &val); + if (ret < 0) { + dev_err(charger->dev, "%s: Error in reading reg 0x%x\n", + __func__, TPS65217_REG_STATUS); + return IRQ_HANDLED; + } + + dev_dbg(charger->dev, "%s: 0x%x\n", __func__, val); + + /* check for charger status bit */ + if (val & CHARGER_STATUS_PRESENT) { + ret = tps65217_enable_charging(charger); + if (ret) { + dev_err(charger->dev, + "failed to enable charger: %d\n", ret); + return IRQ_HANDLED; + } + } else { + charger->online = 0; + } + + if (charger->prev_online != charger->online) + power_supply_changed(charger->psy); + + ret = tps65217_reg_read(charger->tps, TPS65217_REG_CHGCONFIG0, &val); + if (ret < 0) { + dev_err(charger->dev, "%s: Error in reading reg 0x%x\n", + __func__, TPS65217_REG_CHGCONFIG0); + return IRQ_HANDLED; + } + + if (val & TPS65217_CHGCONFIG0_ACTIVE) + dev_dbg(charger->dev, "%s: charger is charging\n", __func__); + else + dev_dbg(charger->dev, + "%s: charger is NOT charging\n", __func__); + + return IRQ_HANDLED; +} + +static int tps65217_charger_poll_task(void *data) +{ + set_freezable(); + + while (!kthread_should_stop()) { + schedule_timeout_interruptible(POLL_INTERVAL); + try_to_freeze(); + tps65217_charger_irq(-1, data); + } + return 0; +} + +static const struct power_supply_desc tps65217_charger_desc = { + .name = "tps65217-charger", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = tps65217_charger_get_property, + .properties = tps65217_charger_props, + .num_properties = ARRAY_SIZE(tps65217_charger_props), +}; + +static int tps65217_charger_probe(struct platform_device *pdev) +{ + struct tps65217 *tps = dev_get_drvdata(pdev->dev.parent); + struct tps65217_charger *charger; + struct power_supply_config cfg = {}; + struct task_struct *poll_task; + int irq[NUM_CHARGER_IRQS]; + int ret; + int i; + + charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); + if (!charger) + return -ENOMEM; + + platform_set_drvdata(pdev, charger); + charger->tps = tps; + charger->dev = &pdev->dev; + + cfg.of_node = pdev->dev.of_node; + cfg.drv_data = charger; + + charger->psy = devm_power_supply_register(&pdev->dev, + &tps65217_charger_desc, + &cfg); + if (IS_ERR(charger->psy)) { + dev_err(&pdev->dev, "failed: power supply register\n"); + return PTR_ERR(charger->psy); + } + + irq[0] = platform_get_irq_byname(pdev, "USB"); + irq[1] = platform_get_irq_byname(pdev, "AC"); + + ret = tps65217_config_charger(charger); + if (ret < 0) { + dev_err(charger->dev, "charger config failed, err %d\n", ret); + return ret; + } + + /* Create a polling thread if an interrupt is invalid */ + if (irq[0] < 0 || irq[1] < 0) { + poll_task = kthread_run(tps65217_charger_poll_task, + charger, "ktps65217charger"); + if (IS_ERR(poll_task)) { + ret = PTR_ERR(poll_task); + dev_err(charger->dev, + "Unable to run kthread err %d\n", ret); + return ret; + } + + charger->poll_task = poll_task; + return 0; + } + + /* Create IRQ threads for charger interrupts */ + for (i = 0; i < NUM_CHARGER_IRQS; i++) { + ret = devm_request_threaded_irq(&pdev->dev, irq[i], NULL, + tps65217_charger_irq, + IRQF_ONESHOT, "tps65217-charger", + charger); + if (ret) { + dev_err(charger->dev, + "Unable to register irq %d err %d\n", irq[i], + ret); + return ret; + } + + /* Check current state */ + tps65217_charger_irq(-1, charger); + } + + return 0; +} + +static int tps65217_charger_remove(struct platform_device *pdev) +{ + struct tps65217_charger *charger = platform_get_drvdata(pdev); + + if (charger->poll_task) + kthread_stop(charger->poll_task); + + return 0; +} + +static const struct of_device_id tps65217_charger_match_table[] = { + { .compatible = "ti,tps65217-charger", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, tps65217_charger_match_table); + +static struct platform_driver tps65217_charger_driver = { + .probe = tps65217_charger_probe, + .remove = tps65217_charger_remove, + .driver = { + .name = "tps65217-charger", + .of_match_table = of_match_ptr(tps65217_charger_match_table), + }, + +}; +module_platform_driver(tps65217_charger_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Enric Balletbo Serra <enric.balletbo@collabora.com>"); +MODULE_DESCRIPTION("TPS65217 battery charger driver"); diff --git a/drivers/power/supply/twl4030_charger.c b/drivers/power/supply/twl4030_charger.c new file mode 100644 index 000000000..1bc49b2e1 --- /dev/null +++ b/drivers/power/supply/twl4030_charger.c @@ -0,0 +1,1150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * TWL4030/TPS65950 BCI (Battery Charger Interface) driver + * + * Copyright (C) 2010 Gražvydas Ignotas <notasas@gmail.com> + * + * based on twl4030_bci_battery.c by TI + * Copyright (C) 2008 Texas Instruments, Inc. + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/interrupt.h> +#include <linux/mfd/twl.h> +#include <linux/power_supply.h> +#include <linux/notifier.h> +#include <linux/usb/otg.h> +#include <linux/iio/consumer.h> + +#define TWL4030_BCIMDEN 0x00 +#define TWL4030_BCIMDKEY 0x01 +#define TWL4030_BCIMSTATEC 0x02 +#define TWL4030_BCIICHG 0x08 +#define TWL4030_BCIVAC 0x0a +#define TWL4030_BCIVBUS 0x0c +#define TWL4030_BCIMFSTS3 0x0F +#define TWL4030_BCIMFSTS4 0x10 +#define TWL4030_BCICTL1 0x23 +#define TWL4030_BB_CFG 0x12 +#define TWL4030_BCIIREF1 0x27 +#define TWL4030_BCIIREF2 0x28 +#define TWL4030_BCIMFKEY 0x11 +#define TWL4030_BCIMFEN3 0x14 +#define TWL4030_BCIMFTH8 0x1d +#define TWL4030_BCIMFTH9 0x1e +#define TWL4030_BCIWDKEY 0x21 + +#define TWL4030_BCIMFSTS1 0x01 + +#define TWL4030_BCIAUTOWEN BIT(5) +#define TWL4030_CONFIG_DONE BIT(4) +#define TWL4030_CVENAC BIT(2) +#define TWL4030_BCIAUTOUSB BIT(1) +#define TWL4030_BCIAUTOAC BIT(0) +#define TWL4030_CGAIN BIT(5) +#define TWL4030_USBFASTMCHG BIT(2) +#define TWL4030_STS_VBUS BIT(7) +#define TWL4030_STS_USB_ID BIT(2) +#define TWL4030_BBCHEN BIT(4) +#define TWL4030_BBSEL_MASK 0x0c +#define TWL4030_BBSEL_2V5 0x00 +#define TWL4030_BBSEL_3V0 0x04 +#define TWL4030_BBSEL_3V1 0x08 +#define TWL4030_BBSEL_3V2 0x0c +#define TWL4030_BBISEL_MASK 0x03 +#define TWL4030_BBISEL_25uA 0x00 +#define TWL4030_BBISEL_150uA 0x01 +#define TWL4030_BBISEL_500uA 0x02 +#define TWL4030_BBISEL_1000uA 0x03 + +#define TWL4030_BATSTSPCHG BIT(2) +#define TWL4030_BATSTSMCHG BIT(6) + +/* BCI interrupts */ +#define TWL4030_WOVF BIT(0) /* Watchdog overflow */ +#define TWL4030_TMOVF BIT(1) /* Timer overflow */ +#define TWL4030_ICHGHIGH BIT(2) /* Battery charge current high */ +#define TWL4030_ICHGLOW BIT(3) /* Battery cc. low / FSM state change */ +#define TWL4030_ICHGEOC BIT(4) /* Battery current end-of-charge */ +#define TWL4030_TBATOR2 BIT(5) /* Battery temperature out of range 2 */ +#define TWL4030_TBATOR1 BIT(6) /* Battery temperature out of range 1 */ +#define TWL4030_BATSTS BIT(7) /* Battery status */ + +#define TWL4030_VBATLVL BIT(0) /* VBAT level */ +#define TWL4030_VBATOV BIT(1) /* VBAT overvoltage */ +#define TWL4030_VBUSOV BIT(2) /* VBUS overvoltage */ +#define TWL4030_ACCHGOV BIT(3) /* Ac charger overvoltage */ + +#define TWL4030_MSTATEC_USB BIT(4) +#define TWL4030_MSTATEC_AC BIT(5) +#define TWL4030_MSTATEC_MASK 0x0f +#define TWL4030_MSTATEC_QUICK1 0x02 +#define TWL4030_MSTATEC_QUICK7 0x07 +#define TWL4030_MSTATEC_COMPLETE1 0x0b +#define TWL4030_MSTATEC_COMPLETE4 0x0e + +/* + * If AC (Accessory Charger) voltage exceeds 4.5V (MADC 11) + * then AC is available. + */ +static inline int ac_available(struct iio_channel *channel_vac) +{ + int val, err; + + if (!channel_vac) + return 0; + + err = iio_read_channel_processed(channel_vac, &val); + if (err < 0) + return 0; + return val > 4500; +} + +static bool allow_usb; +module_param(allow_usb, bool, 0644); +MODULE_PARM_DESC(allow_usb, "Allow USB charge drawing default current"); + +struct twl4030_bci { + struct device *dev; + struct power_supply *ac; + struct power_supply *usb; + struct usb_phy *transceiver; + struct notifier_block usb_nb; + struct work_struct work; + int irq_chg; + int irq_bci; + int usb_enabled; + + /* + * ichg_* and *_cur values in uA. If any are 'large', we set + * CGAIN to '1' which doubles the range for half the + * precision. + */ + unsigned int ichg_eoc, ichg_lo, ichg_hi; + unsigned int usb_cur, ac_cur; + struct iio_channel *channel_vac; + bool ac_is_active; + int usb_mode, ac_mode; /* charging mode requested */ +#define CHARGE_OFF 0 +#define CHARGE_AUTO 1 +#define CHARGE_LINEAR 2 + + /* When setting the USB current we slowly increase the + * requested current until target is reached or the voltage + * drops below 4.75V. In the latter case we step back one + * step. + */ + unsigned int usb_cur_target; + struct delayed_work current_worker; +#define USB_CUR_STEP 20000 /* 20mA at a time */ +#define USB_MIN_VOLT 4750000 /* 4.75V */ +#define USB_CUR_DELAY msecs_to_jiffies(100) +#define USB_MAX_CURRENT 1700000 /* TWL4030 caps at 1.7A */ + + unsigned long event; +}; + +/* strings for 'usb_mode' values */ +static const char *modes[] = { "off", "auto", "continuous" }; + +/* + * clear and set bits on an given register on a given module + */ +static int twl4030_clear_set(u8 mod_no, u8 clear, u8 set, u8 reg) +{ + u8 val = 0; + int ret; + + ret = twl_i2c_read_u8(mod_no, &val, reg); + if (ret) + return ret; + + val &= ~clear; + val |= set; + + return twl_i2c_write_u8(mod_no, val, reg); +} + +static int twl4030_bci_read(u8 reg, u8 *val) +{ + return twl_i2c_read_u8(TWL_MODULE_MAIN_CHARGE, val, reg); +} + +static int twl4030_clear_set_boot_bci(u8 clear, u8 set) +{ + return twl4030_clear_set(TWL_MODULE_PM_MASTER, clear, + TWL4030_CONFIG_DONE | TWL4030_BCIAUTOWEN | set, + TWL4030_PM_MASTER_BOOT_BCI); +} + +static int twl4030bci_read_adc_val(u8 reg) +{ + int ret, temp; + u8 val; + + /* read MSB */ + ret = twl4030_bci_read(reg + 1, &val); + if (ret) + return ret; + + temp = (int)(val & 0x03) << 8; + + /* read LSB */ + ret = twl4030_bci_read(reg, &val); + if (ret) + return ret; + + return temp | val; +} + +/* + * TI provided formulas: + * CGAIN == 0: ICHG = (BCIICHG * 1.7) / (2^10 - 1) - 0.85 + * CGAIN == 1: ICHG = (BCIICHG * 3.4) / (2^10 - 1) - 1.7 + * Here we use integer approximation of: + * CGAIN == 0: val * 1.6618 - 0.85 * 1000 + * CGAIN == 1: (val * 1.6618 - 0.85 * 1000) * 2 + */ +/* + * convert twl register value for currents into uA + */ +static int regval2ua(int regval, bool cgain) +{ + if (cgain) + return (regval * 16618 - 8500 * 1000) / 5; + else + return (regval * 16618 - 8500 * 1000) / 10; +} + +/* + * convert uA currents into twl register value + */ +static int ua2regval(int ua, bool cgain) +{ + int ret; + if (cgain) + ua /= 2; + ret = (ua * 10 + 8500 * 1000) / 16618; + /* rounding problems */ + if (ret < 512) + ret = 512; + return ret; +} + +static int twl4030_charger_update_current(struct twl4030_bci *bci) +{ + int status; + int cur; + unsigned reg, cur_reg; + u8 bcictl1, oldreg, fullreg; + bool cgain = false; + u8 boot_bci; + + /* + * If AC (Accessory Charger) voltage exceeds 4.5V (MADC 11) + * and AC is enabled, set current for 'ac' + */ + if (ac_available(bci->channel_vac)) { + cur = bci->ac_cur; + bci->ac_is_active = true; + } else { + cur = bci->usb_cur; + bci->ac_is_active = false; + if (cur > bci->usb_cur_target) { + cur = bci->usb_cur_target; + bci->usb_cur = cur; + } + if (cur < bci->usb_cur_target) + schedule_delayed_work(&bci->current_worker, USB_CUR_DELAY); + } + + /* First, check thresholds and see if cgain is needed */ + if (bci->ichg_eoc >= 200000) + cgain = true; + if (bci->ichg_lo >= 400000) + cgain = true; + if (bci->ichg_hi >= 820000) + cgain = true; + if (cur > 852000) + cgain = true; + + status = twl4030_bci_read(TWL4030_BCICTL1, &bcictl1); + if (status < 0) + return status; + if (twl_i2c_read_u8(TWL_MODULE_PM_MASTER, &boot_bci, + TWL4030_PM_MASTER_BOOT_BCI) < 0) + boot_bci = 0; + boot_bci &= 7; + + if ((!!cgain) != !!(bcictl1 & TWL4030_CGAIN)) + /* Need to turn for charging while we change the + * CGAIN bit. Leave it off while everything is + * updated. + */ + twl4030_clear_set_boot_bci(boot_bci, 0); + + /* + * For ichg_eoc, the hardware only supports reg values matching + * 100XXXX000, and requires the XXXX be stored in the high nibble + * of TWL4030_BCIMFTH8. + */ + reg = ua2regval(bci->ichg_eoc, cgain); + if (reg > 0x278) + reg = 0x278; + if (reg < 0x200) + reg = 0x200; + reg = (reg >> 3) & 0xf; + fullreg = reg << 4; + + /* + * For ichg_lo, reg value must match 10XXXX0000. + * XXXX is stored in low nibble of TWL4030_BCIMFTH8. + */ + reg = ua2regval(bci->ichg_lo, cgain); + if (reg > 0x2F0) + reg = 0x2F0; + if (reg < 0x200) + reg = 0x200; + reg = (reg >> 4) & 0xf; + fullreg |= reg; + + /* ichg_eoc and ichg_lo live in same register */ + status = twl4030_bci_read(TWL4030_BCIMFTH8, &oldreg); + if (status < 0) + return status; + if (oldreg != fullreg) { + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xF4, + TWL4030_BCIMFKEY); + if (status < 0) + return status; + twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, + fullreg, TWL4030_BCIMFTH8); + } + + /* ichg_hi threshold must be 1XXXX01100 (I think) */ + reg = ua2regval(bci->ichg_hi, cgain); + if (reg > 0x3E0) + reg = 0x3E0; + if (reg < 0x200) + reg = 0x200; + fullreg = (reg >> 5) & 0xF; + fullreg <<= 4; + status = twl4030_bci_read(TWL4030_BCIMFTH9, &oldreg); + if (status < 0) + return status; + if ((oldreg & 0xF0) != fullreg) { + fullreg |= (oldreg & 0x0F); + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xE7, + TWL4030_BCIMFKEY); + if (status < 0) + return status; + twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, + fullreg, TWL4030_BCIMFTH9); + } + + /* + * And finally, set the current. This is stored in + * two registers. + */ + reg = ua2regval(cur, cgain); + /* we have only 10 bits */ + if (reg > 0x3ff) + reg = 0x3ff; + status = twl4030_bci_read(TWL4030_BCIIREF1, &oldreg); + if (status < 0) + return status; + cur_reg = oldreg; + status = twl4030_bci_read(TWL4030_BCIIREF2, &oldreg); + if (status < 0) + return status; + cur_reg |= oldreg << 8; + if (reg != oldreg) { + /* disable write protection for one write access for + * BCIIREF */ + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xE7, + TWL4030_BCIMFKEY); + if (status < 0) + return status; + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, + (reg & 0x100) ? 3 : 2, + TWL4030_BCIIREF2); + if (status < 0) + return status; + /* disable write protection for one write access for + * BCIIREF */ + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xE7, + TWL4030_BCIMFKEY); + if (status < 0) + return status; + status = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, + reg & 0xff, + TWL4030_BCIIREF1); + } + if ((!!cgain) != !!(bcictl1 & TWL4030_CGAIN)) { + /* Flip CGAIN and re-enable charging */ + bcictl1 ^= TWL4030_CGAIN; + twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, + bcictl1, TWL4030_BCICTL1); + twl4030_clear_set_boot_bci(0, boot_bci); + } + return 0; +} + +static int twl4030_charger_get_current(void); + +static void twl4030_current_worker(struct work_struct *data) +{ + int v, curr; + int res; + struct twl4030_bci *bci = container_of(data, struct twl4030_bci, + current_worker.work); + + res = twl4030bci_read_adc_val(TWL4030_BCIVBUS); + if (res < 0) + v = 0; + else + /* BCIVBUS uses ADCIN8, 7/1023 V/step */ + v = res * 6843; + curr = twl4030_charger_get_current(); + + dev_dbg(bci->dev, "v=%d cur=%d limit=%d target=%d\n", v, curr, + bci->usb_cur, bci->usb_cur_target); + + if (v < USB_MIN_VOLT) { + /* Back up and stop adjusting. */ + if (bci->usb_cur >= USB_CUR_STEP) + bci->usb_cur -= USB_CUR_STEP; + bci->usb_cur_target = bci->usb_cur; + } else if (bci->usb_cur >= bci->usb_cur_target || + bci->usb_cur + USB_CUR_STEP > USB_MAX_CURRENT) { + /* Reached target and voltage is OK - stop */ + return; + } else { + bci->usb_cur += USB_CUR_STEP; + schedule_delayed_work(&bci->current_worker, USB_CUR_DELAY); + } + twl4030_charger_update_current(bci); +} + +/* + * Enable/Disable USB Charge functionality. + */ +static int twl4030_charger_enable_usb(struct twl4030_bci *bci, bool enable) +{ + int ret; + u32 reg; + + if (bci->usb_mode == CHARGE_OFF) + enable = false; + if (enable && !IS_ERR_OR_NULL(bci->transceiver)) { + + twl4030_charger_update_current(bci); + + /* Need to keep phy powered */ + if (!bci->usb_enabled) { + pm_runtime_get_sync(bci->transceiver->dev); + bci->usb_enabled = 1; + } + + if (bci->usb_mode == CHARGE_AUTO) { + /* Enable interrupts now. */ + reg = ~(u32)(TWL4030_ICHGLOW | TWL4030_ICHGEOC | + TWL4030_TBATOR2 | TWL4030_TBATOR1 | + TWL4030_BATSTS); + ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg, + TWL4030_INTERRUPTS_BCIIMR1A); + if (ret < 0) { + dev_err(bci->dev, + "failed to unmask interrupts: %d\n", + ret); + return ret; + } + /* forcing the field BCIAUTOUSB (BOOT_BCI[1]) to 1 */ + ret = twl4030_clear_set_boot_bci(0, TWL4030_BCIAUTOUSB); + } + + /* forcing USBFASTMCHG(BCIMFSTS4[2]) to 1 */ + ret = twl4030_clear_set(TWL_MODULE_MAIN_CHARGE, 0, + TWL4030_USBFASTMCHG, TWL4030_BCIMFSTS4); + if (bci->usb_mode == CHARGE_LINEAR) { + /* Enable interrupts now. */ + reg = ~(u32)(TWL4030_ICHGLOW | TWL4030_TBATOR2 | + TWL4030_TBATOR1 | TWL4030_BATSTS); + ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg, + TWL4030_INTERRUPTS_BCIIMR1A); + if (ret < 0) { + dev_err(bci->dev, + "failed to unmask interrupts: %d\n", + ret); + return ret; + } + twl4030_clear_set_boot_bci(TWL4030_BCIAUTOAC|TWL4030_CVENAC, 0); + /* Watch dog key: WOVF acknowledge */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0x33, + TWL4030_BCIWDKEY); + /* 0x24 + EKEY6: off mode */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0x2a, + TWL4030_BCIMDKEY); + /* EKEY2: Linear charge: USB path */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0x26, + TWL4030_BCIMDKEY); + /* WDKEY5: stop watchdog count */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xf3, + TWL4030_BCIWDKEY); + /* enable MFEN3 access */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0x9c, + TWL4030_BCIMFKEY); + /* ICHGEOCEN - end-of-charge monitor (current < 80mA) + * (charging continues) + * ICHGLOWEN - current level monitor (charge continues) + * don't monitor over-current or heat save + */ + ret = twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0xf0, + TWL4030_BCIMFEN3); + } + } else { + ret = twl4030_clear_set_boot_bci(TWL4030_BCIAUTOUSB, 0); + ret |= twl_i2c_write_u8(TWL_MODULE_MAIN_CHARGE, 0x2a, + TWL4030_BCIMDKEY); + if (bci->usb_enabled) { + pm_runtime_mark_last_busy(bci->transceiver->dev); + pm_runtime_put_autosuspend(bci->transceiver->dev); + bci->usb_enabled = 0; + } + bci->usb_cur = 0; + } + + return ret; +} + +/* + * Enable/Disable AC Charge funtionality. + */ +static int twl4030_charger_enable_ac(struct twl4030_bci *bci, bool enable) +{ + int ret; + + if (bci->ac_mode == CHARGE_OFF) + enable = false; + + if (enable) + ret = twl4030_clear_set_boot_bci(0, TWL4030_BCIAUTOAC); + else + ret = twl4030_clear_set_boot_bci(TWL4030_BCIAUTOAC, 0); + + return ret; +} + +/* + * Enable/Disable charging of Backup Battery. + */ +static int twl4030_charger_enable_backup(int uvolt, int uamp) +{ + int ret; + u8 flags; + + if (uvolt < 2500000 || + uamp < 25) { + /* disable charging of backup battery */ + ret = twl4030_clear_set(TWL_MODULE_PM_RECEIVER, + TWL4030_BBCHEN, 0, TWL4030_BB_CFG); + return ret; + } + + flags = TWL4030_BBCHEN; + if (uvolt >= 3200000) + flags |= TWL4030_BBSEL_3V2; + else if (uvolt >= 3100000) + flags |= TWL4030_BBSEL_3V1; + else if (uvolt >= 3000000) + flags |= TWL4030_BBSEL_3V0; + else + flags |= TWL4030_BBSEL_2V5; + + if (uamp >= 1000) + flags |= TWL4030_BBISEL_1000uA; + else if (uamp >= 500) + flags |= TWL4030_BBISEL_500uA; + else if (uamp >= 150) + flags |= TWL4030_BBISEL_150uA; + else + flags |= TWL4030_BBISEL_25uA; + + ret = twl4030_clear_set(TWL_MODULE_PM_RECEIVER, + TWL4030_BBSEL_MASK | TWL4030_BBISEL_MASK, + flags, + TWL4030_BB_CFG); + + return ret; +} + +/* + * TWL4030 CHG_PRES (AC charger presence) events + */ +static irqreturn_t twl4030_charger_interrupt(int irq, void *arg) +{ + struct twl4030_bci *bci = arg; + + dev_dbg(bci->dev, "CHG_PRES irq\n"); + /* reset current on each 'plug' event */ + bci->ac_cur = 500000; + twl4030_charger_update_current(bci); + power_supply_changed(bci->ac); + power_supply_changed(bci->usb); + + return IRQ_HANDLED; +} + +/* + * TWL4030 BCI monitoring events + */ +static irqreturn_t twl4030_bci_interrupt(int irq, void *arg) +{ + struct twl4030_bci *bci = arg; + u8 irqs1, irqs2; + int ret; + + ret = twl_i2c_read_u8(TWL4030_MODULE_INTERRUPTS, &irqs1, + TWL4030_INTERRUPTS_BCIISR1A); + if (ret < 0) + return IRQ_HANDLED; + + ret = twl_i2c_read_u8(TWL4030_MODULE_INTERRUPTS, &irqs2, + TWL4030_INTERRUPTS_BCIISR2A); + if (ret < 0) + return IRQ_HANDLED; + + dev_dbg(bci->dev, "BCI irq %02x %02x\n", irqs2, irqs1); + + if (irqs1 & (TWL4030_ICHGLOW | TWL4030_ICHGEOC)) { + /* charger state change, inform the core */ + power_supply_changed(bci->ac); + power_supply_changed(bci->usb); + } + twl4030_charger_update_current(bci); + + /* various monitoring events, for now we just log them here */ + if (irqs1 & (TWL4030_TBATOR2 | TWL4030_TBATOR1)) + dev_warn(bci->dev, "battery temperature out of range\n"); + + if (irqs1 & TWL4030_BATSTS) + dev_crit(bci->dev, "battery disconnected\n"); + + if (irqs2 & TWL4030_VBATOV) + dev_crit(bci->dev, "VBAT overvoltage\n"); + + if (irqs2 & TWL4030_VBUSOV) + dev_crit(bci->dev, "VBUS overvoltage\n"); + + if (irqs2 & TWL4030_ACCHGOV) + dev_crit(bci->dev, "Ac charger overvoltage\n"); + + return IRQ_HANDLED; +} + +static void twl4030_bci_usb_work(struct work_struct *data) +{ + struct twl4030_bci *bci = container_of(data, struct twl4030_bci, work); + + switch (bci->event) { + case USB_EVENT_VBUS: + case USB_EVENT_CHARGER: + twl4030_charger_enable_usb(bci, true); + break; + case USB_EVENT_NONE: + twl4030_charger_enable_usb(bci, false); + break; + } +} + +static int twl4030_bci_usb_ncb(struct notifier_block *nb, unsigned long val, + void *priv) +{ + struct twl4030_bci *bci = container_of(nb, struct twl4030_bci, usb_nb); + + dev_dbg(bci->dev, "OTG notify %lu\n", val); + + /* reset current on each 'plug' event */ + if (allow_usb) + bci->usb_cur_target = 500000; + else + bci->usb_cur_target = 100000; + + bci->event = val; + schedule_work(&bci->work); + + return NOTIFY_OK; +} + +/* + * sysfs charger enabled store + */ +static ssize_t +twl4030_bci_mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t n) +{ + struct twl4030_bci *bci = dev_get_drvdata(dev->parent); + int mode; + int status; + + mode = sysfs_match_string(modes, buf); + if (mode < 0) + return mode; + + if (dev == &bci->ac->dev) { + if (mode == 2) + return -EINVAL; + twl4030_charger_enable_ac(bci, false); + bci->ac_mode = mode; + status = twl4030_charger_enable_ac(bci, true); + } else { + twl4030_charger_enable_usb(bci, false); + bci->usb_mode = mode; + status = twl4030_charger_enable_usb(bci, true); + } + return (status == 0) ? n : status; +} + +/* + * sysfs charger enabled show + */ +static ssize_t +twl4030_bci_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct twl4030_bci *bci = dev_get_drvdata(dev->parent); + int len = 0; + int i; + int mode = bci->usb_mode; + + if (dev == &bci->ac->dev) + mode = bci->ac_mode; + + for (i = 0; i < ARRAY_SIZE(modes); i++) + if (mode == i) + len += scnprintf(buf+len, PAGE_SIZE-len, + "[%s] ", modes[i]); + else + len += scnprintf(buf+len, PAGE_SIZE-len, + "%s ", modes[i]); + buf[len-1] = '\n'; + return len; +} +static DEVICE_ATTR(mode, 0644, twl4030_bci_mode_show, + twl4030_bci_mode_store); + +static int twl4030_charger_get_current(void) +{ + int curr; + int ret; + u8 bcictl1; + + curr = twl4030bci_read_adc_val(TWL4030_BCIICHG); + if (curr < 0) + return curr; + + ret = twl4030_bci_read(TWL4030_BCICTL1, &bcictl1); + if (ret) + return ret; + + return regval2ua(curr, bcictl1 & TWL4030_CGAIN); +} + +/* + * Returns the main charge FSM state + * Or < 0 on failure. + */ +static int twl4030bci_state(struct twl4030_bci *bci) +{ + int ret; + u8 state; + + ret = twl4030_bci_read(TWL4030_BCIMSTATEC, &state); + if (ret) { + dev_err(bci->dev, "error reading BCIMSTATEC\n"); + return ret; + } + + dev_dbg(bci->dev, "state: %02x\n", state); + + return state; +} + +static int twl4030_bci_state_to_status(int state) +{ + state &= TWL4030_MSTATEC_MASK; + if (TWL4030_MSTATEC_QUICK1 <= state && state <= TWL4030_MSTATEC_QUICK7) + return POWER_SUPPLY_STATUS_CHARGING; + else if (TWL4030_MSTATEC_COMPLETE1 <= state && + state <= TWL4030_MSTATEC_COMPLETE4) + return POWER_SUPPLY_STATUS_FULL; + else + return POWER_SUPPLY_STATUS_NOT_CHARGING; +} + +static int twl4030_bci_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct twl4030_bci *bci = dev_get_drvdata(psy->dev.parent); + int is_charging; + int state; + int ret; + + state = twl4030bci_state(bci); + if (state < 0) + return state; + + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) + is_charging = state & TWL4030_MSTATEC_USB; + else + is_charging = state & TWL4030_MSTATEC_AC; + if (!is_charging) { + u8 s; + ret = twl4030_bci_read(TWL4030_BCIMDEN, &s); + if (ret < 0) + return ret; + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) + is_charging = s & 1; + else + is_charging = s & 2; + if (is_charging) + /* A little white lie */ + state = TWL4030_MSTATEC_QUICK1; + } + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (is_charging) + val->intval = twl4030_bci_state_to_status(state); + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + /* charging must be active for meaningful result */ + if (!is_charging) + return -ENODATA; + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) { + ret = twl4030bci_read_adc_val(TWL4030_BCIVBUS); + if (ret < 0) + return ret; + /* BCIVBUS uses ADCIN8, 7/1023 V/step */ + val->intval = ret * 6843; + } else { + ret = twl4030bci_read_adc_val(TWL4030_BCIVAC); + if (ret < 0) + return ret; + /* BCIVAC uses ADCIN11, 10/1023 V/step */ + val->intval = ret * 9775; + } + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + if (!is_charging) + return -ENODATA; + /* current measurement is shared between AC and USB */ + ret = twl4030_charger_get_current(); + if (ret < 0) + return ret; + val->intval = ret; + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = is_charging && + twl4030_bci_state_to_status(state) != + POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + val->intval = -1; + if (psy->desc->type != POWER_SUPPLY_TYPE_USB) { + if (!bci->ac_is_active) + val->intval = bci->ac_cur; + } else { + if (bci->ac_is_active) + val->intval = bci->usb_cur_target; + } + if (val->intval < 0) { + u8 bcictl1; + + val->intval = twl4030bci_read_adc_val(TWL4030_BCIIREF1); + if (val->intval < 0) + return val->intval; + ret = twl4030_bci_read(TWL4030_BCICTL1, &bcictl1); + if (ret < 0) + return ret; + val->intval = regval2ua(val->intval, bcictl1 & + TWL4030_CGAIN); + } + break; + default: + return -EINVAL; + } + + return 0; +} + +static int twl4030_bci_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct twl4030_bci *bci = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (psy->desc->type == POWER_SUPPLY_TYPE_USB) + bci->usb_cur_target = val->intval; + else + bci->ac_cur = val->intval; + twl4030_charger_update_current(bci); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int twl4030_bci_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return true; + default: + return false; + } +} + +static enum power_supply_property twl4030_charger_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, +}; + +#ifdef CONFIG_OF +static const struct twl4030_bci_platform_data * +twl4030_bci_parse_dt(struct device *dev) +{ + struct device_node *np = dev->of_node; + struct twl4030_bci_platform_data *pdata; + u32 num; + + if (!np) + return NULL; + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return pdata; + + if (of_property_read_u32(np, "ti,bb-uvolt", &num) == 0) + pdata->bb_uvolt = num; + if (of_property_read_u32(np, "ti,bb-uamp", &num) == 0) + pdata->bb_uamp = num; + return pdata; +} +#else +static inline const struct twl4030_bci_platform_data * +twl4030_bci_parse_dt(struct device *dev) +{ + return NULL; +} +#endif + +static const struct power_supply_desc twl4030_bci_ac_desc = { + .name = "twl4030_ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = twl4030_charger_props, + .num_properties = ARRAY_SIZE(twl4030_charger_props), + .get_property = twl4030_bci_get_property, + .set_property = twl4030_bci_set_property, + .property_is_writeable = twl4030_bci_property_is_writeable, +}; + +static const struct power_supply_desc twl4030_bci_usb_desc = { + .name = "twl4030_usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = twl4030_charger_props, + .num_properties = ARRAY_SIZE(twl4030_charger_props), + .get_property = twl4030_bci_get_property, + .set_property = twl4030_bci_set_property, + .property_is_writeable = twl4030_bci_property_is_writeable, +}; + +static int twl4030_bci_probe(struct platform_device *pdev) +{ + struct twl4030_bci *bci; + const struct twl4030_bci_platform_data *pdata = pdev->dev.platform_data; + int ret; + u32 reg; + + bci = devm_kzalloc(&pdev->dev, sizeof(*bci), GFP_KERNEL); + if (bci == NULL) + return -ENOMEM; + + if (!pdata) + pdata = twl4030_bci_parse_dt(&pdev->dev); + + bci->ichg_eoc = 80100; /* Stop charging when current drops to here */ + bci->ichg_lo = 241000; /* Low threshold */ + bci->ichg_hi = 500000; /* High threshold */ + bci->ac_cur = 500000; /* 500mA */ + if (allow_usb) + bci->usb_cur_target = 500000; /* 500mA */ + else + bci->usb_cur_target = 100000; /* 100mA */ + bci->usb_mode = CHARGE_AUTO; + bci->ac_mode = CHARGE_AUTO; + + bci->dev = &pdev->dev; + bci->irq_chg = platform_get_irq(pdev, 0); + bci->irq_bci = platform_get_irq(pdev, 1); + + platform_set_drvdata(pdev, bci); + + INIT_WORK(&bci->work, twl4030_bci_usb_work); + INIT_DELAYED_WORK(&bci->current_worker, twl4030_current_worker); + + bci->channel_vac = devm_iio_channel_get(&pdev->dev, "vac"); + if (IS_ERR(bci->channel_vac)) { + ret = PTR_ERR(bci->channel_vac); + if (ret == -EPROBE_DEFER) + return ret; /* iio not ready */ + dev_warn(&pdev->dev, "could not request vac iio channel (%d)", + ret); + bci->channel_vac = NULL; + } + + if (bci->dev->of_node) { + struct device_node *phynode; + + phynode = of_get_compatible_child(bci->dev->of_node->parent, + "ti,twl4030-usb"); + if (phynode) { + bci->usb_nb.notifier_call = twl4030_bci_usb_ncb; + bci->transceiver = devm_usb_get_phy_by_node( + bci->dev, phynode, &bci->usb_nb); + of_node_put(phynode); + if (IS_ERR(bci->transceiver)) { + ret = PTR_ERR(bci->transceiver); + if (ret == -EPROBE_DEFER) + return ret; /* phy not ready */ + dev_warn(&pdev->dev, "could not request transceiver (%d)", + ret); + bci->transceiver = NULL; + } + } + } + + bci->ac = devm_power_supply_register(&pdev->dev, &twl4030_bci_ac_desc, + NULL); + if (IS_ERR(bci->ac)) { + ret = PTR_ERR(bci->ac); + dev_err(&pdev->dev, "failed to register ac: %d\n", ret); + return ret; + } + + bci->usb = devm_power_supply_register(&pdev->dev, &twl4030_bci_usb_desc, + NULL); + if (IS_ERR(bci->usb)) { + ret = PTR_ERR(bci->usb); + dev_err(&pdev->dev, "failed to register usb: %d\n", ret); + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, bci->irq_chg, NULL, + twl4030_charger_interrupt, IRQF_ONESHOT, pdev->name, + bci); + if (ret < 0) { + dev_err(&pdev->dev, "could not request irq %d, status %d\n", + bci->irq_chg, ret); + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, bci->irq_bci, NULL, + twl4030_bci_interrupt, IRQF_ONESHOT, pdev->name, bci); + if (ret < 0) { + dev_err(&pdev->dev, "could not request irq %d, status %d\n", + bci->irq_bci, ret); + return ret; + } + + /* Enable interrupts now. */ + reg = ~(u32)(TWL4030_ICHGLOW | TWL4030_ICHGEOC | TWL4030_TBATOR2 | + TWL4030_TBATOR1 | TWL4030_BATSTS); + ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg, + TWL4030_INTERRUPTS_BCIIMR1A); + if (ret < 0) { + dev_err(&pdev->dev, "failed to unmask interrupts: %d\n", ret); + return ret; + } + + reg = ~(u32)(TWL4030_VBATOV | TWL4030_VBUSOV | TWL4030_ACCHGOV); + ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg, + TWL4030_INTERRUPTS_BCIIMR2A); + if (ret < 0) + dev_warn(&pdev->dev, "failed to unmask interrupts: %d\n", ret); + + twl4030_charger_update_current(bci); + if (device_create_file(&bci->usb->dev, &dev_attr_mode)) + dev_warn(&pdev->dev, "could not create sysfs file\n"); + if (device_create_file(&bci->ac->dev, &dev_attr_mode)) + dev_warn(&pdev->dev, "could not create sysfs file\n"); + + twl4030_charger_enable_ac(bci, true); + if (!IS_ERR_OR_NULL(bci->transceiver)) + twl4030_bci_usb_ncb(&bci->usb_nb, + bci->transceiver->last_event, + NULL); + else + twl4030_charger_enable_usb(bci, false); + if (pdata) + twl4030_charger_enable_backup(pdata->bb_uvolt, + pdata->bb_uamp); + else + twl4030_charger_enable_backup(0, 0); + + return 0; +} + +static int twl4030_bci_remove(struct platform_device *pdev) +{ + struct twl4030_bci *bci = platform_get_drvdata(pdev); + + twl4030_charger_enable_ac(bci, false); + twl4030_charger_enable_usb(bci, false); + twl4030_charger_enable_backup(0, 0); + + device_remove_file(&bci->usb->dev, &dev_attr_mode); + device_remove_file(&bci->ac->dev, &dev_attr_mode); + /* mask interrupts */ + twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, 0xff, + TWL4030_INTERRUPTS_BCIIMR1A); + twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, 0xff, + TWL4030_INTERRUPTS_BCIIMR2A); + + return 0; +} + +static const struct of_device_id twl_bci_of_match[] = { + {.compatible = "ti,twl4030-bci", }, + { } +}; +MODULE_DEVICE_TABLE(of, twl_bci_of_match); + +static struct platform_driver twl4030_bci_driver = { + .probe = twl4030_bci_probe, + .remove = twl4030_bci_remove, + .driver = { + .name = "twl4030_bci", + .of_match_table = of_match_ptr(twl_bci_of_match), + }, +}; +module_platform_driver(twl4030_bci_driver); + +MODULE_AUTHOR("Gražvydas Ignotas"); +MODULE_DESCRIPTION("TWL4030 Battery Charger Interface driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:twl4030_bci"); diff --git a/drivers/power/supply/twl4030_madc_battery.c b/drivers/power/supply/twl4030_madc_battery.c new file mode 100644 index 000000000..48649dcfe --- /dev/null +++ b/drivers/power/supply/twl4030_madc_battery.c @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Dumb driver for LiIon batteries using TWL4030 madc. + * + * Copyright 2013 Golden Delicious Computers + * Lukas Märdian <lukas@goldelico.com> + * + * Based on dumb driver for gta01 battery + * Copyright 2009 Openmoko, Inc + * Balaji Rao <balajirrao@openmoko.org> + */ + +#include <linux/module.h> +#include <linux/param.h> +#include <linux/delay.h> +#include <linux/workqueue.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/sort.h> +#include <linux/power/twl4030_madc_battery.h> +#include <linux/iio/consumer.h> + +struct twl4030_madc_battery { + struct power_supply *psy; + struct twl4030_madc_bat_platform_data *pdata; + struct iio_channel *channel_temp; + struct iio_channel *channel_ichg; + struct iio_channel *channel_vbat; +}; + +static enum power_supply_property twl4030_madc_bat_props[] = { + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, +}; + +static int madc_read(struct iio_channel *channel) +{ + int val, err; + err = iio_read_channel_processed(channel, &val); + if (err < 0) + return err; + + return val; +} + +static int twl4030_madc_bat_get_charging_status(struct twl4030_madc_battery *bt) +{ + return (madc_read(bt->channel_ichg) > 0) ? 1 : 0; +} + +static int twl4030_madc_bat_get_voltage(struct twl4030_madc_battery *bt) +{ + return madc_read(bt->channel_vbat); +} + +static int twl4030_madc_bat_get_current(struct twl4030_madc_battery *bt) +{ + return madc_read(bt->channel_ichg) * 1000; +} + +static int twl4030_madc_bat_get_temp(struct twl4030_madc_battery *bt) +{ + return madc_read(bt->channel_temp) * 10; +} + +static int twl4030_madc_bat_voltscale(struct twl4030_madc_battery *bat, + int volt) +{ + struct twl4030_madc_bat_calibration *calibration; + int i, res = 0; + + /* choose charging curve */ + if (twl4030_madc_bat_get_charging_status(bat)) + calibration = bat->pdata->charging; + else + calibration = bat->pdata->discharging; + + if (volt > calibration[0].voltage) { + res = calibration[0].level; + } else { + for (i = 0; calibration[i+1].voltage >= 0; i++) { + if (volt <= calibration[i].voltage && + volt >= calibration[i+1].voltage) { + /* interval found - interpolate within range */ + res = calibration[i].level - + ((calibration[i].voltage - volt) * + (calibration[i].level - + calibration[i+1].level)) / + (calibration[i].voltage - + calibration[i+1].voltage); + break; + } + } + } + return res; +} + +static int twl4030_madc_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct twl4030_madc_battery *bat = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (twl4030_madc_bat_voltscale(bat, + twl4030_madc_bat_get_voltage(bat)) > 95) + val->intval = POWER_SUPPLY_STATUS_FULL; + else { + if (twl4030_madc_bat_get_charging_status(bat)) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + } + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = twl4030_madc_bat_get_voltage(bat) * 1000; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = twl4030_madc_bat_get_current(bat); + break; + case POWER_SUPPLY_PROP_PRESENT: + /* assume battery is always present */ + val->intval = 1; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: { + int percent = twl4030_madc_bat_voltscale(bat, + twl4030_madc_bat_get_voltage(bat)); + val->intval = (percent * bat->pdata->capacity) / 100; + break; + } + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = twl4030_madc_bat_voltscale(bat, + twl4030_madc_bat_get_voltage(bat)); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + val->intval = bat->pdata->capacity; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = twl4030_madc_bat_get_temp(bat); + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: { + int percent = twl4030_madc_bat_voltscale(bat, + twl4030_madc_bat_get_voltage(bat)); + /* in mAh */ + int chg = (percent * (bat->pdata->capacity/1000))/100; + + /* assume discharge with 400 mA (ca. 1.5W) */ + val->intval = (3600l * chg) / 400; + break; + } + default: + return -EINVAL; + } + + return 0; +} + +static void twl4030_madc_bat_ext_changed(struct power_supply *psy) +{ + power_supply_changed(psy); +} + +static const struct power_supply_desc twl4030_madc_bat_desc = { + .name = "twl4030_battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = twl4030_madc_bat_props, + .num_properties = ARRAY_SIZE(twl4030_madc_bat_props), + .get_property = twl4030_madc_bat_get_property, + .external_power_changed = twl4030_madc_bat_ext_changed, + +}; + +static int twl4030_cmp(const void *a, const void *b) +{ + return ((struct twl4030_madc_bat_calibration *)b)->voltage - + ((struct twl4030_madc_bat_calibration *)a)->voltage; +} + +static int twl4030_madc_battery_probe(struct platform_device *pdev) +{ + struct twl4030_madc_battery *twl4030_madc_bat; + struct twl4030_madc_bat_platform_data *pdata = pdev->dev.platform_data; + struct power_supply_config psy_cfg = {}; + int ret = 0; + + twl4030_madc_bat = devm_kzalloc(&pdev->dev, sizeof(*twl4030_madc_bat), + GFP_KERNEL); + if (!twl4030_madc_bat) + return -ENOMEM; + + twl4030_madc_bat->channel_temp = iio_channel_get(&pdev->dev, "temp"); + if (IS_ERR(twl4030_madc_bat->channel_temp)) { + ret = PTR_ERR(twl4030_madc_bat->channel_temp); + goto err; + } + + twl4030_madc_bat->channel_ichg = iio_channel_get(&pdev->dev, "ichg"); + if (IS_ERR(twl4030_madc_bat->channel_ichg)) { + ret = PTR_ERR(twl4030_madc_bat->channel_ichg); + goto err_temp; + } + + twl4030_madc_bat->channel_vbat = iio_channel_get(&pdev->dev, "vbat"); + if (IS_ERR(twl4030_madc_bat->channel_vbat)) { + ret = PTR_ERR(twl4030_madc_bat->channel_vbat); + goto err_ichg; + } + + /* sort charging and discharging calibration data */ + sort(pdata->charging, pdata->charging_size, + sizeof(struct twl4030_madc_bat_calibration), + twl4030_cmp, NULL); + sort(pdata->discharging, pdata->discharging_size, + sizeof(struct twl4030_madc_bat_calibration), + twl4030_cmp, NULL); + + twl4030_madc_bat->pdata = pdata; + platform_set_drvdata(pdev, twl4030_madc_bat); + psy_cfg.drv_data = twl4030_madc_bat; + twl4030_madc_bat->psy = power_supply_register(&pdev->dev, + &twl4030_madc_bat_desc, + &psy_cfg); + if (IS_ERR(twl4030_madc_bat->psy)) { + ret = PTR_ERR(twl4030_madc_bat->psy); + goto err_vbat; + } + + return 0; + +err_vbat: + iio_channel_release(twl4030_madc_bat->channel_vbat); +err_ichg: + iio_channel_release(twl4030_madc_bat->channel_ichg); +err_temp: + iio_channel_release(twl4030_madc_bat->channel_temp); +err: + return ret; +} + +static int twl4030_madc_battery_remove(struct platform_device *pdev) +{ + struct twl4030_madc_battery *bat = platform_get_drvdata(pdev); + + power_supply_unregister(bat->psy); + + iio_channel_release(bat->channel_vbat); + iio_channel_release(bat->channel_ichg); + iio_channel_release(bat->channel_temp); + + return 0; +} + +static struct platform_driver twl4030_madc_battery_driver = { + .driver = { + .name = "twl4030_madc_battery", + }, + .probe = twl4030_madc_battery_probe, + .remove = twl4030_madc_battery_remove, +}; +module_platform_driver(twl4030_madc_battery_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Lukas Märdian <lukas@goldelico.com>"); +MODULE_DESCRIPTION("twl4030_madc battery driver"); +MODULE_ALIAS("platform:twl4030_madc_battery"); diff --git a/drivers/power/supply/ucs1002_power.c b/drivers/power/supply/ucs1002_power.c new file mode 100644 index 000000000..332cb50d9 --- /dev/null +++ b/drivers/power/supply/ucs1002_power.c @@ -0,0 +1,692 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for UCS1002 Programmable USB Port Power Controller + * + * Copyright (C) 2019 Zodiac Inflight Innovations + */ +#include <linux/bits.h> +#include <linux/freezer.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/kthread.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_irq.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/regulator/driver.h> +#include <linux/regulator/of_regulator.h> + +/* UCS1002 Registers */ +#define UCS1002_REG_CURRENT_MEASUREMENT 0x00 + +/* + * The Total Accumulated Charge registers store the total accumulated + * charge delivered from the VS source to a portable device. The total + * value is calculated using four registers, from 01h to 04h. The bit + * weighting of the registers is given in mA/hrs. + */ +#define UCS1002_REG_TOTAL_ACC_CHARGE 0x01 + +/* Other Status Register */ +#define UCS1002_REG_OTHER_STATUS 0x0f +# define F_ADET_PIN BIT(4) +# define F_CHG_ACT BIT(3) + +/* Interrupt Status */ +#define UCS1002_REG_INTERRUPT_STATUS 0x10 +# define F_ERR BIT(7) +# define F_DISCHARGE_ERR BIT(6) +# define F_RESET BIT(5) +# define F_MIN_KEEP_OUT BIT(4) +# define F_TSD BIT(3) +# define F_OVER_VOLT BIT(2) +# define F_BACK_VOLT BIT(1) +# define F_OVER_ILIM BIT(0) + +/* Pin Status Register */ +#define UCS1002_REG_PIN_STATUS 0x14 +# define UCS1002_PWR_STATE_MASK 0x03 +# define F_PWR_EN_PIN BIT(6) +# define F_M2_PIN BIT(5) +# define F_M1_PIN BIT(4) +# define F_EM_EN_PIN BIT(3) +# define F_SEL_PIN BIT(2) +# define F_ACTIVE_MODE_MASK GENMASK(5, 3) +# define F_ACTIVE_MODE_PASSTHROUGH F_M2_PIN +# define F_ACTIVE_MODE_DEDICATED F_EM_EN_PIN +# define F_ACTIVE_MODE_BC12_DCP (F_M2_PIN | F_EM_EN_PIN) +# define F_ACTIVE_MODE_BC12_SDP F_M1_PIN +# define F_ACTIVE_MODE_BC12_CDP (F_M1_PIN | F_M2_PIN | F_EM_EN_PIN) + +/* General Configuration Register */ +#define UCS1002_REG_GENERAL_CFG 0x15 +# define F_RATION_EN BIT(3) + +/* Emulation Configuration Register */ +#define UCS1002_REG_EMU_CFG 0x16 + +/* Switch Configuration Register */ +#define UCS1002_REG_SWITCH_CFG 0x17 +# define F_PIN_IGNORE BIT(7) +# define F_EM_EN_SET BIT(5) +# define F_M2_SET BIT(4) +# define F_M1_SET BIT(3) +# define F_S0_SET BIT(2) +# define F_PWR_EN_SET BIT(1) +# define F_LATCH_SET BIT(0) +# define V_SET_ACTIVE_MODE_MASK GENMASK(5, 3) +# define V_SET_ACTIVE_MODE_PASSTHROUGH F_M2_SET +# define V_SET_ACTIVE_MODE_DEDICATED F_EM_EN_SET +# define V_SET_ACTIVE_MODE_BC12_DCP (F_M2_SET | F_EM_EN_SET) +# define V_SET_ACTIVE_MODE_BC12_SDP F_M1_SET +# define V_SET_ACTIVE_MODE_BC12_CDP (F_M1_SET | F_M2_SET | F_EM_EN_SET) + +/* Current Limit Register */ +#define UCS1002_REG_ILIMIT 0x19 +# define UCS1002_ILIM_SW_MASK GENMASK(3, 0) + +/* Product ID */ +#define UCS1002_REG_PRODUCT_ID 0xfd +# define UCS1002_PRODUCT_ID 0x4e + +/* Manufacture name */ +#define UCS1002_MANUFACTURER "SMSC" + +struct ucs1002_info { + struct power_supply *charger; + struct i2c_client *client; + struct regmap *regmap; + struct regulator_desc *regulator_descriptor; + struct regulator_dev *rdev; + bool present; + bool output_disable; + struct delayed_work health_poll; + int health; + +}; + +static enum power_supply_property ucs1002_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_PRESENT, /* the presence of PED */ + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_HEALTH, +}; + +static int ucs1002_get_online(struct ucs1002_info *info, + union power_supply_propval *val) +{ + unsigned int reg; + int ret; + + ret = regmap_read(info->regmap, UCS1002_REG_OTHER_STATUS, ®); + if (ret) + return ret; + + val->intval = !!(reg & F_CHG_ACT); + + return 0; +} + +static int ucs1002_get_charge(struct ucs1002_info *info, + union power_supply_propval *val) +{ + /* + * To fit within 32 bits some values are rounded (uA/h) + * + * For Total Accumulated Charge Middle Low Byte register, addr + * 03h, byte 2 + * + * B0: 0.01084 mA/h rounded to 11 uA/h + * B1: 0.02169 mA/h rounded to 22 uA/h + * B2: 0.04340 mA/h rounded to 43 uA/h + * B3: 0.08676 mA/h rounded to 87 uA/h + * B4: 0.17350 mA/h rounded to 173 uÁ/h + * + * For Total Accumulated Charge Low Byte register, addr 04h, + * byte 3 + * + * B6: 0.00271 mA/h rounded to 3 uA/h + * B7: 0.005422 mA/h rounded to 5 uA/h + */ + static const int bit_weights_uAh[BITS_PER_TYPE(u32)] = { + /* + * Bit corresponding to low byte (offset 0x04) + * B0 B1 B2 B3 B4 B5 B6 B7 + */ + 0, 0, 0, 0, 0, 0, 3, 5, + /* + * Bit corresponding to middle low byte (offset 0x03) + * B0 B1 B2 B3 B4 B5 B6 B7 + */ + 11, 22, 43, 87, 173, 347, 694, 1388, + /* + * Bit corresponding to middle high byte (offset 0x02) + * B0 B1 B2 B3 B4 B5 B6 B7 + */ + 2776, 5552, 11105, 22210, 44420, 88840, 177700, 355400, + /* + * Bit corresponding to high byte (offset 0x01) + * B0 B1 B2 B3 B4 B5 B6 B7 + */ + 710700, 1421000, 2843000, 5685000, 11371000, 22742000, + 45484000, 90968000, + }; + unsigned long total_acc_charger; + unsigned int reg; + int i, ret; + + ret = regmap_bulk_read(info->regmap, UCS1002_REG_TOTAL_ACC_CHARGE, + ®, sizeof(u32)); + if (ret) + return ret; + + total_acc_charger = be32_to_cpu(reg); /* BE as per offsets above */ + val->intval = 0; + + for_each_set_bit(i, &total_acc_charger, ARRAY_SIZE(bit_weights_uAh)) + val->intval += bit_weights_uAh[i]; + + return 0; +} + +static int ucs1002_get_current(struct ucs1002_info *info, + union power_supply_propval *val) +{ + /* + * The Current Measurement register stores the measured + * current value delivered to the portable device. The range + * is from 9.76 mA to 2.5 A. + */ + static const int bit_weights_uA[BITS_PER_TYPE(u8)] = { + 9760, 19500, 39000, 78100, 156200, 312300, 624600, 1249300, + }; + unsigned long current_measurement; + unsigned int reg; + int i, ret; + + ret = regmap_read(info->regmap, UCS1002_REG_CURRENT_MEASUREMENT, ®); + if (ret) + return ret; + + current_measurement = reg; + val->intval = 0; + + for_each_set_bit(i, ¤t_measurement, ARRAY_SIZE(bit_weights_uA)) + val->intval += bit_weights_uA[i]; + + return 0; +} + +/* + * The Current Limit register stores the maximum current used by the + * port switch. The range is from 500mA to 2.5 A. + */ +static const u32 ucs1002_current_limit_uA[] = { + 500000, 900000, 1000000, 1200000, 1500000, 1800000, 2000000, 2500000, +}; + +static int ucs1002_get_max_current(struct ucs1002_info *info, + union power_supply_propval *val) +{ + unsigned int reg; + int ret; + + if (info->output_disable) { + val->intval = 0; + return 0; + } + + ret = regmap_read(info->regmap, UCS1002_REG_ILIMIT, ®); + if (ret) + return ret; + + val->intval = ucs1002_current_limit_uA[reg & UCS1002_ILIM_SW_MASK]; + + return 0; +} + +static int ucs1002_set_max_current(struct ucs1002_info *info, u32 val) +{ + unsigned int reg; + int ret, idx; + + if (val == 0) { + info->output_disable = true; + regulator_disable_regmap(info->rdev); + return 0; + } + + for (idx = 0; idx < ARRAY_SIZE(ucs1002_current_limit_uA); idx++) { + if (val == ucs1002_current_limit_uA[idx]) + break; + } + + if (idx == ARRAY_SIZE(ucs1002_current_limit_uA)) + return -EINVAL; + + ret = regmap_write(info->regmap, UCS1002_REG_ILIMIT, idx); + if (ret) + return ret; + /* + * Any current limit setting exceeding the one set via ILIM + * pin will be rejected, so we read out freshly changed limit + * to make sure that it took effect. + */ + ret = regmap_read(info->regmap, UCS1002_REG_ILIMIT, ®); + if (ret) + return ret; + + if (reg != idx) + return -EINVAL; + + info->output_disable = false; + + if (info->rdev && info->rdev->use_count && + !regulator_is_enabled_regmap(info->rdev)) + regulator_enable_regmap(info->rdev); + + return 0; +} + +static enum power_supply_usb_type ucs1002_usb_types[] = { + POWER_SUPPLY_USB_TYPE_PD, + POWER_SUPPLY_USB_TYPE_SDP, + POWER_SUPPLY_USB_TYPE_DCP, + POWER_SUPPLY_USB_TYPE_CDP, + POWER_SUPPLY_USB_TYPE_UNKNOWN, +}; + +static int ucs1002_set_usb_type(struct ucs1002_info *info, int val) +{ + unsigned int mode; + + if (val < 0 || val >= ARRAY_SIZE(ucs1002_usb_types)) + return -EINVAL; + + switch (ucs1002_usb_types[val]) { + case POWER_SUPPLY_USB_TYPE_PD: + mode = V_SET_ACTIVE_MODE_DEDICATED; + break; + case POWER_SUPPLY_USB_TYPE_SDP: + mode = V_SET_ACTIVE_MODE_BC12_SDP; + break; + case POWER_SUPPLY_USB_TYPE_DCP: + mode = V_SET_ACTIVE_MODE_BC12_DCP; + break; + case POWER_SUPPLY_USB_TYPE_CDP: + mode = V_SET_ACTIVE_MODE_BC12_CDP; + break; + default: + return -EINVAL; + } + + return regmap_update_bits(info->regmap, UCS1002_REG_SWITCH_CFG, + V_SET_ACTIVE_MODE_MASK, mode); +} + +static int ucs1002_get_usb_type(struct ucs1002_info *info, + union power_supply_propval *val) +{ + enum power_supply_usb_type type; + unsigned int reg; + int ret; + + ret = regmap_read(info->regmap, UCS1002_REG_PIN_STATUS, ®); + if (ret) + return ret; + + switch (reg & F_ACTIVE_MODE_MASK) { + default: + type = POWER_SUPPLY_USB_TYPE_UNKNOWN; + break; + case F_ACTIVE_MODE_DEDICATED: + type = POWER_SUPPLY_USB_TYPE_PD; + break; + case F_ACTIVE_MODE_BC12_SDP: + type = POWER_SUPPLY_USB_TYPE_SDP; + break; + case F_ACTIVE_MODE_BC12_DCP: + type = POWER_SUPPLY_USB_TYPE_DCP; + break; + case F_ACTIVE_MODE_BC12_CDP: + type = POWER_SUPPLY_USB_TYPE_CDP; + break; + } + + val->intval = type; + + return 0; +} + +static int ucs1002_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct ucs1002_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + return ucs1002_get_online(info, val); + case POWER_SUPPLY_PROP_CHARGE_NOW: + return ucs1002_get_charge(info, val); + case POWER_SUPPLY_PROP_CURRENT_NOW: + return ucs1002_get_current(info, val); + case POWER_SUPPLY_PROP_CURRENT_MAX: + return ucs1002_get_max_current(info, val); + case POWER_SUPPLY_PROP_USB_TYPE: + return ucs1002_get_usb_type(info, val); + case POWER_SUPPLY_PROP_HEALTH: + val->intval = info->health; + return 0; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = info->present; + return 0; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = UCS1002_MANUFACTURER; + return 0; + default: + return -EINVAL; + } +} + +static int ucs1002_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct ucs1002_info *info = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CURRENT_MAX: + return ucs1002_set_max_current(info, val->intval); + case POWER_SUPPLY_PROP_USB_TYPE: + return ucs1002_set_usb_type(info, val->intval); + default: + return -EINVAL; + } +} + +static int ucs1002_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CURRENT_MAX: + case POWER_SUPPLY_PROP_USB_TYPE: + return true; + default: + return false; + } +} + +static const struct power_supply_desc ucs1002_charger_desc = { + .name = "ucs1002", + .type = POWER_SUPPLY_TYPE_USB, + .usb_types = ucs1002_usb_types, + .num_usb_types = ARRAY_SIZE(ucs1002_usb_types), + .get_property = ucs1002_get_property, + .set_property = ucs1002_set_property, + .property_is_writeable = ucs1002_property_is_writeable, + .properties = ucs1002_props, + .num_properties = ARRAY_SIZE(ucs1002_props), +}; + +static void ucs1002_health_poll(struct work_struct *work) +{ + struct ucs1002_info *info = container_of(work, struct ucs1002_info, + health_poll.work); + int ret; + u32 reg; + + ret = regmap_read(info->regmap, UCS1002_REG_INTERRUPT_STATUS, ®); + if (ret) + return; + + /* bad health and no status change, just schedule us again in a while */ + if ((reg & F_ERR) && info->health != POWER_SUPPLY_HEALTH_GOOD) { + schedule_delayed_work(&info->health_poll, + msecs_to_jiffies(2000)); + return; + } + + if (reg & F_TSD) + info->health = POWER_SUPPLY_HEALTH_OVERHEAT; + else if (reg & (F_OVER_VOLT | F_BACK_VOLT)) + info->health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (reg & F_OVER_ILIM) + info->health = POWER_SUPPLY_HEALTH_OVERCURRENT; + else if (reg & (F_DISCHARGE_ERR | F_MIN_KEEP_OUT)) + info->health = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + else + info->health = POWER_SUPPLY_HEALTH_GOOD; + + sysfs_notify(&info->charger->dev.kobj, NULL, "health"); +} + +static irqreturn_t ucs1002_charger_irq(int irq, void *data) +{ + int ret, regval; + bool present; + struct ucs1002_info *info = data; + + present = info->present; + + ret = regmap_read(info->regmap, UCS1002_REG_OTHER_STATUS, ®val); + if (ret) + return IRQ_HANDLED; + + /* update attached status */ + info->present = regval & F_ADET_PIN; + + /* notify the change */ + if (present != info->present) + power_supply_changed(info->charger); + + return IRQ_HANDLED; +} + +static irqreturn_t ucs1002_alert_irq(int irq, void *data) +{ + struct ucs1002_info *info = data; + + mod_delayed_work(system_wq, &info->health_poll, 0); + + return IRQ_HANDLED; +} + +static int ucs1002_regulator_enable(struct regulator_dev *rdev) +{ + struct ucs1002_info *info = rdev_get_drvdata(rdev); + + /* + * If the output is disabled due to 0 maximum current, just pretend the + * enable did work. The regulator will be enabled as soon as we get a + * a non-zero maximum current budget. + */ + if (info->output_disable) + return 0; + + return regulator_enable_regmap(rdev); +} + +static const struct regulator_ops ucs1002_regulator_ops = { + .is_enabled = regulator_is_enabled_regmap, + .enable = ucs1002_regulator_enable, + .disable = regulator_disable_regmap, +}; + +static const struct regulator_desc ucs1002_regulator_descriptor = { + .name = "ucs1002-vbus", + .ops = &ucs1002_regulator_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_reg = UCS1002_REG_SWITCH_CFG, + .enable_mask = F_PWR_EN_SET, + .enable_val = F_PWR_EN_SET, + .fixed_uV = 5000000, + .n_voltages = 1, +}; + +static int ucs1002_probe(struct i2c_client *client, + const struct i2c_device_id *dev_id) +{ + struct device *dev = &client->dev; + struct power_supply_config charger_config = {}; + const struct regmap_config regmap_config = { + .reg_bits = 8, + .val_bits = 8, + }; + struct regulator_config regulator_config = {}; + int irq_a_det, irq_alert, ret; + struct ucs1002_info *info; + unsigned int regval; + + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->regmap = devm_regmap_init_i2c(client, ®map_config); + ret = PTR_ERR_OR_ZERO(info->regmap); + if (ret) { + dev_err(dev, "Regmap initialization failed: %d\n", ret); + return ret; + } + + info->client = client; + + irq_a_det = of_irq_get_byname(dev->of_node, "a_det"); + irq_alert = of_irq_get_byname(dev->of_node, "alert"); + + charger_config.of_node = dev->of_node; + charger_config.drv_data = info; + + ret = regmap_read(info->regmap, UCS1002_REG_PRODUCT_ID, ®val); + if (ret) { + dev_err(dev, "Failed to read product ID: %d\n", ret); + return ret; + } + + if (regval != UCS1002_PRODUCT_ID) { + dev_err(dev, + "Product ID does not match (0x%02x != 0x%02x)\n", + regval, UCS1002_PRODUCT_ID); + return -ENODEV; + } + + /* Enable charge rationing by default */ + ret = regmap_update_bits(info->regmap, UCS1002_REG_GENERAL_CFG, + F_RATION_EN, F_RATION_EN); + if (ret) { + dev_err(dev, "Failed to read general config: %d\n", ret); + return ret; + } + + /* + * Ignore the M1, M2, PWR_EN, and EM_EN pin states. Set active + * mode selection to BC1.2 CDP. + */ + ret = regmap_update_bits(info->regmap, UCS1002_REG_SWITCH_CFG, + V_SET_ACTIVE_MODE_MASK | F_PIN_IGNORE, + V_SET_ACTIVE_MODE_BC12_CDP | F_PIN_IGNORE); + if (ret) { + dev_err(dev, "Failed to configure default mode: %d\n", ret); + return ret; + } + /* + * Be safe and set initial current limit to 500mA + */ + ret = ucs1002_set_max_current(info, 500000); + if (ret) { + dev_err(dev, "Failed to set max current default: %d\n", ret); + return ret; + } + + info->charger = devm_power_supply_register(dev, &ucs1002_charger_desc, + &charger_config); + ret = PTR_ERR_OR_ZERO(info->charger); + if (ret) { + dev_err(dev, "Failed to register power supply: %d\n", ret); + return ret; + } + + ret = regmap_read(info->regmap, UCS1002_REG_PIN_STATUS, ®val); + if (ret) { + dev_err(dev, "Failed to read pin status: %d\n", ret); + return ret; + } + + info->regulator_descriptor = + devm_kmemdup(dev, &ucs1002_regulator_descriptor, + sizeof(ucs1002_regulator_descriptor), + GFP_KERNEL); + if (!info->regulator_descriptor) + return -ENOMEM; + + info->regulator_descriptor->enable_is_inverted = !(regval & F_SEL_PIN); + + regulator_config.dev = dev; + regulator_config.of_node = dev->of_node; + regulator_config.regmap = info->regmap; + regulator_config.driver_data = info; + + info->rdev = devm_regulator_register(dev, info->regulator_descriptor, + ®ulator_config); + ret = PTR_ERR_OR_ZERO(info->rdev); + if (ret) { + dev_err(dev, "Failed to register VBUS regulator: %d\n", ret); + return ret; + } + + info->health = POWER_SUPPLY_HEALTH_GOOD; + INIT_DELAYED_WORK(&info->health_poll, ucs1002_health_poll); + + if (irq_a_det > 0) { + ret = devm_request_threaded_irq(dev, irq_a_det, NULL, + ucs1002_charger_irq, + IRQF_ONESHOT, + "ucs1002-a_det", info); + if (ret) { + dev_err(dev, "Failed to request A_DET threaded irq: %d\n", + ret); + return ret; + } + } + + if (irq_alert > 0) { + ret = devm_request_irq(dev, irq_alert, ucs1002_alert_irq, + 0,"ucs1002-alert", info); + if (ret) { + dev_err(dev, "Failed to request ALERT threaded irq: %d\n", + ret); + return ret; + } + } + + return 0; +} + +static const struct of_device_id ucs1002_of_match[] = { + { .compatible = "microchip,ucs1002", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, ucs1002_of_match); + +static struct i2c_driver ucs1002_driver = { + .driver = { + .name = "ucs1002", + .of_match_table = ucs1002_of_match, + }, + .probe = ucs1002_probe, +}; +module_i2c_driver(ucs1002_driver); + +MODULE_DESCRIPTION("Microchip UCS1002 Programmable USB Port Power Controller"); +MODULE_AUTHOR("Enric Balletbo Serra <enric.balletbo@collabora.com>"); +MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/power/supply/wilco-charger.c b/drivers/power/supply/wilco-charger.c new file mode 100644 index 000000000..98ade073e --- /dev/null +++ b/drivers/power/supply/wilco-charger.c @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Charging control driver for the Wilco EC + * + * Copyright 2019 Google LLC + * + * See Documentation/ABI/testing/sysfs-class-power and + * Documentation/ABI/testing/sysfs-class-power-wilco for userspace interface + * and other info. + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/platform_data/wilco-ec.h> +#include <linux/power_supply.h> + +#define DRV_NAME "wilco-charger" + +/* Property IDs and related EC constants */ +#define PID_CHARGE_MODE 0x0710 +#define PID_CHARGE_LOWER_LIMIT 0x0711 +#define PID_CHARGE_UPPER_LIMIT 0x0712 + +enum charge_mode { + CHARGE_MODE_STD = 1, /* Used for Standard */ + CHARGE_MODE_EXP = 2, /* Express Charge, used for Fast */ + CHARGE_MODE_AC = 3, /* Mostly AC use, used for Trickle */ + CHARGE_MODE_AUTO = 4, /* Used for Adaptive */ + CHARGE_MODE_CUSTOM = 5, /* Used for Custom */ + CHARGE_MODE_LONGLIFE = 6, /* Used for Long Life */ +}; + +#define CHARGE_LOWER_LIMIT_MIN 50 +#define CHARGE_LOWER_LIMIT_MAX 95 +#define CHARGE_UPPER_LIMIT_MIN 55 +#define CHARGE_UPPER_LIMIT_MAX 100 + +/* Convert from POWER_SUPPLY_PROP_CHARGE_TYPE value to the EC's charge mode */ +static int psp_val_to_charge_mode(int psp_val) +{ + switch (psp_val) { + case POWER_SUPPLY_CHARGE_TYPE_TRICKLE: + return CHARGE_MODE_AC; + case POWER_SUPPLY_CHARGE_TYPE_FAST: + return CHARGE_MODE_EXP; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + return CHARGE_MODE_STD; + case POWER_SUPPLY_CHARGE_TYPE_ADAPTIVE: + return CHARGE_MODE_AUTO; + case POWER_SUPPLY_CHARGE_TYPE_CUSTOM: + return CHARGE_MODE_CUSTOM; + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + return CHARGE_MODE_LONGLIFE; + default: + return -EINVAL; + } +} + +/* Convert from EC's charge mode to POWER_SUPPLY_PROP_CHARGE_TYPE value */ +static int charge_mode_to_psp_val(enum charge_mode mode) +{ + switch (mode) { + case CHARGE_MODE_AC: + return POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + case CHARGE_MODE_EXP: + return POWER_SUPPLY_CHARGE_TYPE_FAST; + case CHARGE_MODE_STD: + return POWER_SUPPLY_CHARGE_TYPE_STANDARD; + case CHARGE_MODE_AUTO: + return POWER_SUPPLY_CHARGE_TYPE_ADAPTIVE; + case CHARGE_MODE_CUSTOM: + return POWER_SUPPLY_CHARGE_TYPE_CUSTOM; + case CHARGE_MODE_LONGLIFE: + return POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + default: + return -EINVAL; + } +} + +static enum power_supply_property wilco_charge_props[] = { + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD, + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + +static int wilco_charge_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wilco_ec_device *ec = power_supply_get_drvdata(psy); + u32 property_id; + int ret; + u8 raw; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPE: + property_id = PID_CHARGE_MODE; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: + property_id = PID_CHARGE_LOWER_LIMIT; + break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + property_id = PID_CHARGE_UPPER_LIMIT; + break; + default: + return -EINVAL; + } + + ret = wilco_ec_get_byte_property(ec, property_id, &raw); + if (ret < 0) + return ret; + if (property_id == PID_CHARGE_MODE) { + ret = charge_mode_to_psp_val(raw); + if (ret < 0) + return -EBADMSG; + raw = ret; + } + val->intval = raw; + + return 0; +} + +static int wilco_charge_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct wilco_ec_device *ec = power_supply_get_drvdata(psy); + int mode; + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_TYPE: + mode = psp_val_to_charge_mode(val->intval); + if (mode < 0) + return -EINVAL; + return wilco_ec_set_byte_property(ec, PID_CHARGE_MODE, mode); + case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: + if (val->intval < CHARGE_LOWER_LIMIT_MIN || + val->intval > CHARGE_LOWER_LIMIT_MAX) + return -EINVAL; + return wilco_ec_set_byte_property(ec, PID_CHARGE_LOWER_LIMIT, + val->intval); + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval < CHARGE_UPPER_LIMIT_MIN || + val->intval > CHARGE_UPPER_LIMIT_MAX) + return -EINVAL; + return wilco_ec_set_byte_property(ec, PID_CHARGE_UPPER_LIMIT, + val->intval); + default: + return -EINVAL; + } +} + +static int wilco_charge_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + return 1; +} + +static const struct power_supply_desc wilco_ps_desc = { + .properties = wilco_charge_props, + .num_properties = ARRAY_SIZE(wilco_charge_props), + .get_property = wilco_charge_get_property, + .set_property = wilco_charge_set_property, + .property_is_writeable = wilco_charge_property_is_writeable, + .name = DRV_NAME, + .type = POWER_SUPPLY_TYPE_MAINS, +}; + +static int wilco_charge_probe(struct platform_device *pdev) +{ + struct wilco_ec_device *ec = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + struct power_supply *psy; + + psy_cfg.drv_data = ec; + psy = devm_power_supply_register(&pdev->dev, &wilco_ps_desc, &psy_cfg); + + return PTR_ERR_OR_ZERO(psy); +} + +static struct platform_driver wilco_charge_driver = { + .probe = wilco_charge_probe, + .driver = { + .name = DRV_NAME, + } +}; +module_platform_driver(wilco_charge_driver); + +MODULE_ALIAS("platform:" DRV_NAME); +MODULE_AUTHOR("Nick Crews <ncrews@chromium.org>"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Wilco EC charge control driver"); diff --git a/drivers/power/supply/wm831x_backup.c b/drivers/power/supply/wm831x_backup.c new file mode 100644 index 000000000..ffb265b85 --- /dev/null +++ b/drivers/power/supply/wm831x_backup.c @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Backup battery driver for Wolfson Microelectronics wm831x PMICs + * + * Copyright 2009 Wolfson Microelectronics PLC. + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> + +#include <linux/mfd/wm831x/core.h> +#include <linux/mfd/wm831x/auxadc.h> +#include <linux/mfd/wm831x/pmu.h> +#include <linux/mfd/wm831x/pdata.h> + +struct wm831x_backup { + struct wm831x *wm831x; + struct power_supply *backup; + struct power_supply_desc backup_desc; + char name[20]; +}; + +static int wm831x_backup_read_voltage(struct wm831x *wm831x, + enum wm831x_auxadc src, + union power_supply_propval *val) +{ + int ret; + + ret = wm831x_auxadc_read_uv(wm831x, src); + if (ret >= 0) + val->intval = ret; + + return ret; +} + +/********************************************************************* + * Backup supply properties + *********************************************************************/ + +static void wm831x_config_backup(struct wm831x *wm831x) +{ + struct wm831x_pdata *wm831x_pdata = wm831x->dev->platform_data; + struct wm831x_backup_pdata *pdata; + int ret, reg; + + if (!wm831x_pdata || !wm831x_pdata->backup) { + dev_warn(wm831x->dev, + "No backup battery charger configuration\n"); + return; + } + + pdata = wm831x_pdata->backup; + + reg = 0; + + if (pdata->charger_enable) + reg |= WM831X_BKUP_CHG_ENA | WM831X_BKUP_BATT_DET_ENA; + if (pdata->no_constant_voltage) + reg |= WM831X_BKUP_CHG_MODE; + + switch (pdata->vlim) { + case 2500: + break; + case 3100: + reg |= WM831X_BKUP_CHG_VLIM; + break; + default: + dev_err(wm831x->dev, "Invalid backup voltage limit %dmV\n", + pdata->vlim); + } + + switch (pdata->ilim) { + case 100: + break; + case 200: + reg |= 1; + break; + case 300: + reg |= 2; + break; + case 400: + reg |= 3; + break; + default: + dev_err(wm831x->dev, "Invalid backup current limit %duA\n", + pdata->ilim); + } + + ret = wm831x_reg_unlock(wm831x); + if (ret != 0) { + dev_err(wm831x->dev, "Failed to unlock registers: %d\n", ret); + return; + } + + ret = wm831x_set_bits(wm831x, WM831X_BACKUP_CHARGER_CONTROL, + WM831X_BKUP_CHG_ENA_MASK | + WM831X_BKUP_CHG_MODE_MASK | + WM831X_BKUP_BATT_DET_ENA_MASK | + WM831X_BKUP_CHG_VLIM_MASK | + WM831X_BKUP_CHG_ILIM_MASK, + reg); + if (ret != 0) + dev_err(wm831x->dev, + "Failed to set backup charger config: %d\n", ret); + + wm831x_reg_lock(wm831x); +} + +static int wm831x_backup_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm831x_backup *devdata = dev_get_drvdata(psy->dev.parent); + struct wm831x *wm831x = devdata->wm831x; + int ret = 0; + + ret = wm831x_reg_read(wm831x, WM831X_BACKUP_CHARGER_CONTROL); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + if (ret & WM831X_BKUP_CHG_STS) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = wm831x_backup_read_voltage(wm831x, WM831X_AUX_BKUP_BATT, + val); + break; + + case POWER_SUPPLY_PROP_PRESENT: + if (ret & WM831X_BKUP_CHG_STS) + val->intval = 1; + else + val->intval = 0; + break; + + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property wm831x_backup_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_PRESENT, +}; + +/********************************************************************* + * Initialisation + *********************************************************************/ + +static int wm831x_backup_probe(struct platform_device *pdev) +{ + struct wm831x *wm831x = dev_get_drvdata(pdev->dev.parent); + struct wm831x_pdata *wm831x_pdata = wm831x->dev->platform_data; + struct wm831x_backup *devdata; + + devdata = devm_kzalloc(&pdev->dev, sizeof(struct wm831x_backup), + GFP_KERNEL); + if (devdata == NULL) + return -ENOMEM; + + devdata->wm831x = wm831x; + platform_set_drvdata(pdev, devdata); + + /* We ignore configuration failures since we can still read + * back the status without enabling the charger (which may + * already be enabled anyway). + */ + wm831x_config_backup(wm831x); + + if (wm831x_pdata && wm831x_pdata->wm831x_num) + snprintf(devdata->name, sizeof(devdata->name), + "wm831x-backup.%d", wm831x_pdata->wm831x_num); + else + snprintf(devdata->name, sizeof(devdata->name), + "wm831x-backup"); + + devdata->backup_desc.name = devdata->name; + devdata->backup_desc.type = POWER_SUPPLY_TYPE_BATTERY; + devdata->backup_desc.properties = wm831x_backup_props; + devdata->backup_desc.num_properties = ARRAY_SIZE(wm831x_backup_props); + devdata->backup_desc.get_property = wm831x_backup_get_prop; + devdata->backup = power_supply_register(&pdev->dev, + &devdata->backup_desc, NULL); + + return PTR_ERR_OR_ZERO(devdata->backup); +} + +static int wm831x_backup_remove(struct platform_device *pdev) +{ + struct wm831x_backup *devdata = platform_get_drvdata(pdev); + + power_supply_unregister(devdata->backup); + + return 0; +} + +static struct platform_driver wm831x_backup_driver = { + .probe = wm831x_backup_probe, + .remove = wm831x_backup_remove, + .driver = { + .name = "wm831x-backup", + }, +}; + +module_platform_driver(wm831x_backup_driver); + +MODULE_DESCRIPTION("Backup battery charger driver for WM831x PMICs"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:wm831x-backup"); diff --git a/drivers/power/supply/wm831x_power.c b/drivers/power/supply/wm831x_power.c new file mode 100644 index 000000000..18b33f14d --- /dev/null +++ b/drivers/power/supply/wm831x_power.c @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * PMU driver for Wolfson Microelectronics wm831x PMICs + * + * Copyright 2009 Wolfson Microelectronics PLC. + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/usb/phy.h> + +#include <linux/mfd/wm831x/core.h> +#include <linux/mfd/wm831x/auxadc.h> +#include <linux/mfd/wm831x/pmu.h> +#include <linux/mfd/wm831x/pdata.h> + +struct wm831x_power { + struct wm831x *wm831x; + struct power_supply *wall; + struct power_supply *usb; + struct power_supply *battery; + struct power_supply_desc wall_desc; + struct power_supply_desc usb_desc; + struct power_supply_desc battery_desc; + char wall_name[20]; + char usb_name[20]; + char battery_name[20]; + bool have_battery; + struct usb_phy *usb_phy; + struct notifier_block usb_notify; +}; + +static int wm831x_power_check_online(struct wm831x *wm831x, int supply, + union power_supply_propval *val) +{ + int ret; + + ret = wm831x_reg_read(wm831x, WM831X_SYSTEM_STATUS); + if (ret < 0) + return ret; + + if (ret & supply) + val->intval = 1; + else + val->intval = 0; + + return 0; +} + +static int wm831x_power_read_voltage(struct wm831x *wm831x, + enum wm831x_auxadc src, + union power_supply_propval *val) +{ + int ret; + + ret = wm831x_auxadc_read_uv(wm831x, src); + if (ret >= 0) + val->intval = ret; + + return ret; +} + +/********************************************************************* + * WALL Power + *********************************************************************/ +static int wm831x_wall_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm831x_power *wm831x_power = dev_get_drvdata(psy->dev.parent); + struct wm831x *wm831x = wm831x_power->wm831x; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = wm831x_power_check_online(wm831x, WM831X_PWR_WALL, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = wm831x_power_read_voltage(wm831x, WM831X_AUX_WALL, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property wm831x_wall_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +/********************************************************************* + * USB Power + *********************************************************************/ +static int wm831x_usb_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm831x_power *wm831x_power = dev_get_drvdata(psy->dev.parent); + struct wm831x *wm831x = wm831x_power->wm831x; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = wm831x_power_check_online(wm831x, WM831X_PWR_USB, val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = wm831x_power_read_voltage(wm831x, WM831X_AUX_USB, val); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property wm831x_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +/* In milliamps */ +static const unsigned int wm831x_usb_limits[] = { + 0, + 2, + 100, + 500, + 900, + 1500, + 1800, + 550, +}; + +static int wm831x_usb_limit_change(struct notifier_block *nb, + unsigned long limit, void *data) +{ + struct wm831x_power *wm831x_power = container_of(nb, + struct wm831x_power, + usb_notify); + unsigned int i, best; + + /* Find the highest supported limit */ + best = 0; + for (i = 0; i < ARRAY_SIZE(wm831x_usb_limits); i++) { + if (limit >= wm831x_usb_limits[i] && + wm831x_usb_limits[best] < wm831x_usb_limits[i]) + best = i; + } + + dev_dbg(wm831x_power->wm831x->dev, + "Limiting USB current to %umA", wm831x_usb_limits[best]); + + wm831x_set_bits(wm831x_power->wm831x, WM831X_POWER_STATE, + WM831X_USB_ILIM_MASK, best); + + return 0; +} + +/********************************************************************* + * Battery properties + *********************************************************************/ + +struct chg_map { + int val; + int reg_val; +}; + +static struct chg_map trickle_ilims[] = { + { 50, 0 << WM831X_CHG_TRKL_ILIM_SHIFT }, + { 100, 1 << WM831X_CHG_TRKL_ILIM_SHIFT }, + { 150, 2 << WM831X_CHG_TRKL_ILIM_SHIFT }, + { 200, 3 << WM831X_CHG_TRKL_ILIM_SHIFT }, +}; + +static struct chg_map vsels[] = { + { 4050, 0 << WM831X_CHG_VSEL_SHIFT }, + { 4100, 1 << WM831X_CHG_VSEL_SHIFT }, + { 4150, 2 << WM831X_CHG_VSEL_SHIFT }, + { 4200, 3 << WM831X_CHG_VSEL_SHIFT }, +}; + +static struct chg_map fast_ilims[] = { + { 0, 0 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 50, 1 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 100, 2 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 150, 3 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 200, 4 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 250, 5 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 300, 6 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 350, 7 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 400, 8 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 450, 9 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 500, 10 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 600, 11 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 700, 12 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 800, 13 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 900, 14 << WM831X_CHG_FAST_ILIM_SHIFT }, + { 1000, 15 << WM831X_CHG_FAST_ILIM_SHIFT }, +}; + +static struct chg_map eoc_iterms[] = { + { 20, 0 << WM831X_CHG_ITERM_SHIFT }, + { 30, 1 << WM831X_CHG_ITERM_SHIFT }, + { 40, 2 << WM831X_CHG_ITERM_SHIFT }, + { 50, 3 << WM831X_CHG_ITERM_SHIFT }, + { 60, 4 << WM831X_CHG_ITERM_SHIFT }, + { 70, 5 << WM831X_CHG_ITERM_SHIFT }, + { 80, 6 << WM831X_CHG_ITERM_SHIFT }, + { 90, 7 << WM831X_CHG_ITERM_SHIFT }, +}; + +static struct chg_map chg_times[] = { + { 60, 0 << WM831X_CHG_TIME_SHIFT }, + { 90, 1 << WM831X_CHG_TIME_SHIFT }, + { 120, 2 << WM831X_CHG_TIME_SHIFT }, + { 150, 3 << WM831X_CHG_TIME_SHIFT }, + { 180, 4 << WM831X_CHG_TIME_SHIFT }, + { 210, 5 << WM831X_CHG_TIME_SHIFT }, + { 240, 6 << WM831X_CHG_TIME_SHIFT }, + { 270, 7 << WM831X_CHG_TIME_SHIFT }, + { 300, 8 << WM831X_CHG_TIME_SHIFT }, + { 330, 9 << WM831X_CHG_TIME_SHIFT }, + { 360, 10 << WM831X_CHG_TIME_SHIFT }, + { 390, 11 << WM831X_CHG_TIME_SHIFT }, + { 420, 12 << WM831X_CHG_TIME_SHIFT }, + { 450, 13 << WM831X_CHG_TIME_SHIFT }, + { 480, 14 << WM831X_CHG_TIME_SHIFT }, + { 510, 15 << WM831X_CHG_TIME_SHIFT }, +}; + +static void wm831x_battey_apply_config(struct wm831x *wm831x, + struct chg_map *map, int count, int val, + int *reg, const char *name, + const char *units) +{ + int i; + + for (i = 0; i < count; i++) + if (val == map[i].val) + break; + if (i == count) { + dev_err(wm831x->dev, "Invalid %s %d%s\n", + name, val, units); + } else { + *reg |= map[i].reg_val; + dev_dbg(wm831x->dev, "Set %s of %d%s\n", name, val, units); + } +} + +static void wm831x_config_battery(struct wm831x *wm831x) +{ + struct wm831x_pdata *wm831x_pdata = wm831x->dev->platform_data; + struct wm831x_battery_pdata *pdata; + int ret, reg1, reg2; + + if (!wm831x_pdata || !wm831x_pdata->battery) { + dev_warn(wm831x->dev, + "No battery charger configuration\n"); + return; + } + + pdata = wm831x_pdata->battery; + + reg1 = 0; + reg2 = 0; + + if (!pdata->enable) { + dev_info(wm831x->dev, "Battery charger disabled\n"); + return; + } + + reg1 |= WM831X_CHG_ENA; + if (pdata->off_mask) + reg2 |= WM831X_CHG_OFF_MSK; + if (pdata->fast_enable) + reg1 |= WM831X_CHG_FAST; + + wm831x_battey_apply_config(wm831x, trickle_ilims, + ARRAY_SIZE(trickle_ilims), + pdata->trickle_ilim, ®2, + "trickle charge current limit", "mA"); + + wm831x_battey_apply_config(wm831x, vsels, ARRAY_SIZE(vsels), + pdata->vsel, ®2, + "target voltage", "mV"); + + wm831x_battey_apply_config(wm831x, fast_ilims, ARRAY_SIZE(fast_ilims), + pdata->fast_ilim, ®2, + "fast charge current limit", "mA"); + + wm831x_battey_apply_config(wm831x, eoc_iterms, ARRAY_SIZE(eoc_iterms), + pdata->eoc_iterm, ®1, + "end of charge current threshold", "mA"); + + wm831x_battey_apply_config(wm831x, chg_times, ARRAY_SIZE(chg_times), + pdata->timeout, ®2, + "charger timeout", "min"); + + ret = wm831x_reg_unlock(wm831x); + if (ret != 0) { + dev_err(wm831x->dev, "Failed to unlock registers: %d\n", ret); + return; + } + + ret = wm831x_set_bits(wm831x, WM831X_CHARGER_CONTROL_1, + WM831X_CHG_ENA_MASK | + WM831X_CHG_FAST_MASK | + WM831X_CHG_ITERM_MASK, + reg1); + if (ret != 0) + dev_err(wm831x->dev, "Failed to set charger control 1: %d\n", + ret); + + ret = wm831x_set_bits(wm831x, WM831X_CHARGER_CONTROL_2, + WM831X_CHG_OFF_MSK | + WM831X_CHG_TIME_MASK | + WM831X_CHG_FAST_ILIM_MASK | + WM831X_CHG_TRKL_ILIM_MASK | + WM831X_CHG_VSEL_MASK, + reg2); + if (ret != 0) + dev_err(wm831x->dev, "Failed to set charger control 2: %d\n", + ret); + + wm831x_reg_lock(wm831x); +} + +static int wm831x_bat_check_status(struct wm831x *wm831x, int *status) +{ + int ret; + + ret = wm831x_reg_read(wm831x, WM831X_SYSTEM_STATUS); + if (ret < 0) + return ret; + + if (ret & WM831X_PWR_SRC_BATT) { + *status = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + } + + ret = wm831x_reg_read(wm831x, WM831X_CHARGER_STATUS); + if (ret < 0) + return ret; + + switch (ret & WM831X_CHG_STATE_MASK) { + case WM831X_CHG_STATE_OFF: + *status = POWER_SUPPLY_STATUS_NOT_CHARGING; + break; + case WM831X_CHG_STATE_TRICKLE: + case WM831X_CHG_STATE_FAST: + *status = POWER_SUPPLY_STATUS_CHARGING; + break; + + default: + *status = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + + return 0; +} + +static int wm831x_bat_check_type(struct wm831x *wm831x, int *type) +{ + int ret; + + ret = wm831x_reg_read(wm831x, WM831X_CHARGER_STATUS); + if (ret < 0) + return ret; + + switch (ret & WM831X_CHG_STATE_MASK) { + case WM831X_CHG_STATE_TRICKLE: + case WM831X_CHG_STATE_TRICKLE_OT: + *type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + break; + case WM831X_CHG_STATE_FAST: + case WM831X_CHG_STATE_FAST_OT: + *type = POWER_SUPPLY_CHARGE_TYPE_FAST; + break; + default: + *type = POWER_SUPPLY_CHARGE_TYPE_NONE; + break; + } + + return 0; +} + +static int wm831x_bat_check_health(struct wm831x *wm831x, int *health) +{ + int ret; + + ret = wm831x_reg_read(wm831x, WM831X_CHARGER_STATUS); + if (ret < 0) + return ret; + + if (ret & WM831X_BATT_HOT_STS) { + *health = POWER_SUPPLY_HEALTH_OVERHEAT; + return 0; + } + + if (ret & WM831X_BATT_COLD_STS) { + *health = POWER_SUPPLY_HEALTH_COLD; + return 0; + } + + if (ret & WM831X_BATT_OV_STS) { + *health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + return 0; + } + + switch (ret & WM831X_CHG_STATE_MASK) { + case WM831X_CHG_STATE_TRICKLE_OT: + case WM831X_CHG_STATE_FAST_OT: + *health = POWER_SUPPLY_HEALTH_OVERHEAT; + break; + case WM831X_CHG_STATE_DEFECTIVE: + *health = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + break; + default: + *health = POWER_SUPPLY_HEALTH_GOOD; + break; + } + + return 0; +} + +static int wm831x_bat_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm831x_power *wm831x_power = dev_get_drvdata(psy->dev.parent); + struct wm831x *wm831x = wm831x_power->wm831x; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + ret = wm831x_bat_check_status(wm831x, &val->intval); + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = wm831x_power_check_online(wm831x, WM831X_PWR_SRC_BATT, + val); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = wm831x_power_read_voltage(wm831x, WM831X_AUX_BATT, val); + break; + case POWER_SUPPLY_PROP_HEALTH: + ret = wm831x_bat_check_health(wm831x, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + ret = wm831x_bat_check_type(wm831x, &val->intval); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property wm831x_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CHARGE_TYPE, +}; + +static const char *wm831x_bat_irqs[] = { + "BATT HOT", + "BATT COLD", + "BATT FAIL", + "OV", + "END", + "TO", + "MODE", + "START", +}; + +static irqreturn_t wm831x_bat_irq(int irq, void *data) +{ + struct wm831x_power *wm831x_power = data; + struct wm831x *wm831x = wm831x_power->wm831x; + + dev_dbg(wm831x->dev, "Battery status changed: %d\n", irq); + + /* The battery charger is autonomous so we don't need to do + * anything except kick user space */ + if (wm831x_power->have_battery) + power_supply_changed(wm831x_power->battery); + + return IRQ_HANDLED; +} + + +/********************************************************************* + * Initialisation + *********************************************************************/ + +static irqreturn_t wm831x_syslo_irq(int irq, void *data) +{ + struct wm831x_power *wm831x_power = data; + struct wm831x *wm831x = wm831x_power->wm831x; + + /* Not much we can actually *do* but tell people for + * posterity, we're probably about to run out of power. */ + dev_crit(wm831x->dev, "SYSVDD under voltage\n"); + + return IRQ_HANDLED; +} + +static irqreturn_t wm831x_pwr_src_irq(int irq, void *data) +{ + struct wm831x_power *wm831x_power = data; + struct wm831x *wm831x = wm831x_power->wm831x; + + dev_dbg(wm831x->dev, "Power source changed\n"); + + /* Just notify for everything - little harm in overnotifying. */ + if (wm831x_power->have_battery) + power_supply_changed(wm831x_power->battery); + power_supply_changed(wm831x_power->usb); + power_supply_changed(wm831x_power->wall); + + return IRQ_HANDLED; +} + +static int wm831x_power_probe(struct platform_device *pdev) +{ + struct wm831x *wm831x = dev_get_drvdata(pdev->dev.parent); + struct wm831x_pdata *wm831x_pdata = wm831x->dev->platform_data; + struct wm831x_power *power; + int ret, irq, i; + + power = devm_kzalloc(&pdev->dev, sizeof(struct wm831x_power), + GFP_KERNEL); + if (power == NULL) + return -ENOMEM; + + power->wm831x = wm831x; + platform_set_drvdata(pdev, power); + + if (wm831x_pdata && wm831x_pdata->wm831x_num) { + snprintf(power->wall_name, sizeof(power->wall_name), + "wm831x-wall.%d", wm831x_pdata->wm831x_num); + snprintf(power->battery_name, sizeof(power->wall_name), + "wm831x-battery.%d", wm831x_pdata->wm831x_num); + snprintf(power->usb_name, sizeof(power->wall_name), + "wm831x-usb.%d", wm831x_pdata->wm831x_num); + } else { + snprintf(power->wall_name, sizeof(power->wall_name), + "wm831x-wall"); + snprintf(power->battery_name, sizeof(power->wall_name), + "wm831x-battery"); + snprintf(power->usb_name, sizeof(power->wall_name), + "wm831x-usb"); + } + + /* We ignore configuration failures since we can still read back + * the status without enabling the charger. + */ + wm831x_config_battery(wm831x); + + power->wall_desc.name = power->wall_name; + power->wall_desc.type = POWER_SUPPLY_TYPE_MAINS; + power->wall_desc.properties = wm831x_wall_props; + power->wall_desc.num_properties = ARRAY_SIZE(wm831x_wall_props); + power->wall_desc.get_property = wm831x_wall_get_prop; + power->wall = power_supply_register(&pdev->dev, &power->wall_desc, + NULL); + if (IS_ERR(power->wall)) { + ret = PTR_ERR(power->wall); + goto err; + } + + power->usb_desc.name = power->usb_name, + power->usb_desc.type = POWER_SUPPLY_TYPE_USB; + power->usb_desc.properties = wm831x_usb_props; + power->usb_desc.num_properties = ARRAY_SIZE(wm831x_usb_props); + power->usb_desc.get_property = wm831x_usb_get_prop; + power->usb = power_supply_register(&pdev->dev, &power->usb_desc, NULL); + if (IS_ERR(power->usb)) { + ret = PTR_ERR(power->usb); + goto err_wall; + } + + ret = wm831x_reg_read(wm831x, WM831X_CHARGER_CONTROL_1); + if (ret < 0) + goto err_wall; + power->have_battery = ret & WM831X_CHG_ENA; + + if (power->have_battery) { + power->battery_desc.name = power->battery_name; + power->battery_desc.properties = wm831x_bat_props; + power->battery_desc.num_properties = ARRAY_SIZE(wm831x_bat_props); + power->battery_desc.get_property = wm831x_bat_get_prop; + power->battery_desc.use_for_apm = 1; + power->battery = power_supply_register(&pdev->dev, + &power->battery_desc, + NULL); + if (IS_ERR(power->battery)) { + ret = PTR_ERR(power->battery); + goto err_usb; + } + } + + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "SYSLO")); + ret = request_threaded_irq(irq, NULL, wm831x_syslo_irq, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, "System power low", + power); + if (ret != 0) { + dev_err(&pdev->dev, "Failed to request SYSLO IRQ %d: %d\n", + irq, ret); + goto err_battery; + } + + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "PWR SRC")); + ret = request_threaded_irq(irq, NULL, wm831x_pwr_src_irq, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, "Power source", + power); + if (ret != 0) { + dev_err(&pdev->dev, "Failed to request PWR SRC IRQ %d: %d\n", + irq, ret); + goto err_syslo; + } + + for (i = 0; i < ARRAY_SIZE(wm831x_bat_irqs); i++) { + irq = wm831x_irq(wm831x, + platform_get_irq_byname(pdev, + wm831x_bat_irqs[i])); + ret = request_threaded_irq(irq, NULL, wm831x_bat_irq, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + wm831x_bat_irqs[i], + power); + if (ret != 0) { + dev_err(&pdev->dev, + "Failed to request %s IRQ %d: %d\n", + wm831x_bat_irqs[i], irq, ret); + goto err_bat_irq; + } + } + + power->usb_phy = devm_usb_get_phy_by_phandle(&pdev->dev, "phys", 0); + ret = PTR_ERR_OR_ZERO(power->usb_phy); + + switch (ret) { + case 0: + power->usb_notify.notifier_call = wm831x_usb_limit_change; + ret = usb_register_notifier(power->usb_phy, &power->usb_notify); + if (ret) { + dev_err(&pdev->dev, "Failed to register notifier: %d\n", + ret); + goto err_bat_irq; + } + break; + case -EINVAL: + case -ENODEV: + /* ignore missing usb-phy, it's optional */ + power->usb_phy = NULL; + ret = 0; + break; + default: + dev_err(&pdev->dev, "Failed to find USB phy: %d\n", ret); + fallthrough; + case -EPROBE_DEFER: + goto err_bat_irq; + break; + } + + return ret; + +err_bat_irq: + --i; + for (; i >= 0; i--) { + irq = platform_get_irq_byname(pdev, wm831x_bat_irqs[i]); + free_irq(irq, power); + } + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "PWR SRC")); + free_irq(irq, power); +err_syslo: + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "SYSLO")); + free_irq(irq, power); +err_battery: + if (power->have_battery) + power_supply_unregister(power->battery); +err_usb: + power_supply_unregister(power->usb); +err_wall: + power_supply_unregister(power->wall); +err: + return ret; +} + +static int wm831x_power_remove(struct platform_device *pdev) +{ + struct wm831x_power *wm831x_power = platform_get_drvdata(pdev); + struct wm831x *wm831x = wm831x_power->wm831x; + int irq, i; + + if (wm831x_power->usb_phy) { + usb_unregister_notifier(wm831x_power->usb_phy, + &wm831x_power->usb_notify); + } + + for (i = 0; i < ARRAY_SIZE(wm831x_bat_irqs); i++) { + irq = wm831x_irq(wm831x, + platform_get_irq_byname(pdev, + wm831x_bat_irqs[i])); + free_irq(irq, wm831x_power); + } + + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "PWR SRC")); + free_irq(irq, wm831x_power); + + irq = wm831x_irq(wm831x, platform_get_irq_byname(pdev, "SYSLO")); + free_irq(irq, wm831x_power); + + if (wm831x_power->have_battery) + power_supply_unregister(wm831x_power->battery); + power_supply_unregister(wm831x_power->wall); + power_supply_unregister(wm831x_power->usb); + return 0; +} + +static struct platform_driver wm831x_power_driver = { + .probe = wm831x_power_probe, + .remove = wm831x_power_remove, + .driver = { + .name = "wm831x-power", + }, +}; + +module_platform_driver(wm831x_power_driver); + +MODULE_DESCRIPTION("Power supply driver for WM831x PMICs"); +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:wm831x-power"); diff --git a/drivers/power/supply/wm8350_power.c b/drivers/power/supply/wm8350_power.c new file mode 100644 index 000000000..908cfd45d --- /dev/null +++ b/drivers/power/supply/wm8350_power.c @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for wm8350 PMIC + * + * Copyright 2007, 2008 Wolfson Microelectronics PLC. + * + * Based on OLPC Battery Driver + * + * Copyright 2006 David Woodhouse <dwmw2@infradead.org> + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/mfd/wm8350/supply.h> +#include <linux/mfd/wm8350/core.h> +#include <linux/mfd/wm8350/comparator.h> + +static int wm8350_read_battery_uvolts(struct wm8350 *wm8350) +{ + return wm8350_read_auxadc(wm8350, WM8350_AUXADC_BATT, 0, 0) + * WM8350_AUX_COEFF; +} + +static int wm8350_read_line_uvolts(struct wm8350 *wm8350) +{ + return wm8350_read_auxadc(wm8350, WM8350_AUXADC_LINE, 0, 0) + * WM8350_AUX_COEFF; +} + +static int wm8350_read_usb_uvolts(struct wm8350 *wm8350) +{ + return wm8350_read_auxadc(wm8350, WM8350_AUXADC_USB, 0, 0) + * WM8350_AUX_COEFF; +} + +#define WM8350_BATT_SUPPLY 1 +#define WM8350_USB_SUPPLY 2 +#define WM8350_LINE_SUPPLY 4 + +static inline int wm8350_charge_time_min(struct wm8350 *wm8350, int min) +{ + if (!wm8350->power.rev_g_coeff) + return (((min - 30) / 15) & 0xf) << 8; + else + return (((min - 30) / 30) & 0xf) << 8; +} + +static int wm8350_get_supplies(struct wm8350 *wm8350) +{ + u16 sm, ov, co, chrg; + int supplies = 0; + + sm = wm8350_reg_read(wm8350, WM8350_STATE_MACHINE_STATUS); + ov = wm8350_reg_read(wm8350, WM8350_MISC_OVERRIDES); + co = wm8350_reg_read(wm8350, WM8350_COMPARATOR_OVERRIDES); + chrg = wm8350_reg_read(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2); + + /* USB_SM */ + sm = (sm & WM8350_USB_SM_MASK) >> WM8350_USB_SM_SHIFT; + + /* CHG_ISEL */ + chrg &= WM8350_CHG_ISEL_MASK; + + /* If the USB state machine is active then we're using that with or + * without battery, otherwise check for wall supply */ + if (((sm == WM8350_USB_SM_100_SLV) || + (sm == WM8350_USB_SM_500_SLV) || + (sm == WM8350_USB_SM_STDBY_SLV)) + && !(ov & WM8350_USB_LIMIT_OVRDE)) + supplies = WM8350_USB_SUPPLY; + else if (((sm == WM8350_USB_SM_100_SLV) || + (sm == WM8350_USB_SM_500_SLV) || + (sm == WM8350_USB_SM_STDBY_SLV)) + && (ov & WM8350_USB_LIMIT_OVRDE) && (chrg == 0)) + supplies = WM8350_USB_SUPPLY | WM8350_BATT_SUPPLY; + else if (co & WM8350_WALL_FB_OVRDE) + supplies = WM8350_LINE_SUPPLY; + else + supplies = WM8350_BATT_SUPPLY; + + return supplies; +} + +static int wm8350_charger_config(struct wm8350 *wm8350, + struct wm8350_charger_policy *policy) +{ + u16 reg, eoc_mA, fast_limit_mA; + + if (!policy) { + dev_warn(wm8350->dev, + "No charger policy, charger not configured.\n"); + return -EINVAL; + } + + /* make sure USB fast charge current is not > 500mA */ + if (policy->fast_limit_USB_mA > 500) { + dev_err(wm8350->dev, "USB fast charge > 500mA\n"); + return -EINVAL; + } + + eoc_mA = WM8350_CHG_EOC_mA(policy->eoc_mA); + + wm8350_reg_unlock(wm8350); + + reg = wm8350_reg_read(wm8350, WM8350_BATTERY_CHARGER_CONTROL_1) + & WM8350_CHG_ENA_R168; + wm8350_reg_write(wm8350, WM8350_BATTERY_CHARGER_CONTROL_1, + reg | eoc_mA | policy->trickle_start_mV | + WM8350_CHG_TRICKLE_TEMP_CHOKE | + WM8350_CHG_TRICKLE_USB_CHOKE | + WM8350_CHG_FAST_USB_THROTTLE); + + if (wm8350_get_supplies(wm8350) & WM8350_USB_SUPPLY) { + fast_limit_mA = + WM8350_CHG_FAST_LIMIT_mA(policy->fast_limit_USB_mA); + wm8350_reg_write(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2, + policy->charge_mV | policy->trickle_charge_USB_mA | + fast_limit_mA | wm8350_charge_time_min(wm8350, + policy->charge_timeout)); + + } else { + fast_limit_mA = + WM8350_CHG_FAST_LIMIT_mA(policy->fast_limit_mA); + wm8350_reg_write(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2, + policy->charge_mV | policy->trickle_charge_mA | + fast_limit_mA | wm8350_charge_time_min(wm8350, + policy->charge_timeout)); + } + + wm8350_reg_lock(wm8350); + return 0; +} + +static int wm8350_batt_status(struct wm8350 *wm8350) +{ + u16 state; + + state = wm8350_reg_read(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2); + state &= WM8350_CHG_STS_MASK; + + switch (state) { + case WM8350_CHG_STS_OFF: + return POWER_SUPPLY_STATUS_DISCHARGING; + + case WM8350_CHG_STS_TRICKLE: + case WM8350_CHG_STS_FAST: + return POWER_SUPPLY_STATUS_CHARGING; + + default: + return POWER_SUPPLY_STATUS_UNKNOWN; + } +} + +static ssize_t charger_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct wm8350 *wm8350 = dev_get_drvdata(dev); + char *charge; + int state; + + state = wm8350_reg_read(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2) & + WM8350_CHG_STS_MASK; + switch (state) { + case WM8350_CHG_STS_OFF: + charge = "Charger Off"; + break; + case WM8350_CHG_STS_TRICKLE: + charge = "Trickle Charging"; + break; + case WM8350_CHG_STS_FAST: + charge = "Fast Charging"; + break; + default: + return 0; + } + + return sprintf(buf, "%s\n", charge); +} + +static DEVICE_ATTR_RO(charger_state); + +static irqreturn_t wm8350_charger_handler(int irq, void *data) +{ + struct wm8350 *wm8350 = data; + struct wm8350_power *power = &wm8350->power; + struct wm8350_charger_policy *policy = power->policy; + + switch (irq - wm8350->irq_base) { + case WM8350_IRQ_CHG_BAT_FAIL: + dev_err(wm8350->dev, "battery failed\n"); + break; + case WM8350_IRQ_CHG_TO: + dev_err(wm8350->dev, "charger timeout\n"); + power_supply_changed(power->battery); + break; + + case WM8350_IRQ_CHG_BAT_HOT: + case WM8350_IRQ_CHG_BAT_COLD: + case WM8350_IRQ_CHG_START: + case WM8350_IRQ_CHG_END: + power_supply_changed(power->battery); + break; + + case WM8350_IRQ_CHG_FAST_RDY: + dev_dbg(wm8350->dev, "fast charger ready\n"); + wm8350_charger_config(wm8350, policy); + wm8350_reg_unlock(wm8350); + wm8350_set_bits(wm8350, WM8350_BATTERY_CHARGER_CONTROL_1, + WM8350_CHG_FAST); + wm8350_reg_lock(wm8350); + break; + + case WM8350_IRQ_CHG_VBATT_LT_3P9: + dev_warn(wm8350->dev, "battery < 3.9V\n"); + break; + case WM8350_IRQ_CHG_VBATT_LT_3P1: + dev_warn(wm8350->dev, "battery < 3.1V\n"); + break; + case WM8350_IRQ_CHG_VBATT_LT_2P85: + dev_warn(wm8350->dev, "battery < 2.85V\n"); + break; + + /* Supply change. We will overnotify but it should do + * no harm. */ + case WM8350_IRQ_EXT_USB_FB: + case WM8350_IRQ_EXT_WALL_FB: + wm8350_charger_config(wm8350, policy); + fallthrough; + case WM8350_IRQ_EXT_BAT_FB: + power_supply_changed(power->battery); + power_supply_changed(power->usb); + power_supply_changed(power->ac); + break; + + default: + dev_err(wm8350->dev, "Unknown interrupt %d\n", irq); + } + + return IRQ_HANDLED; +} + +/********************************************************************* + * AC Power + *********************************************************************/ +static int wm8350_ac_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm8350 *wm8350 = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = !!(wm8350_get_supplies(wm8350) & + WM8350_LINE_SUPPLY); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = wm8350_read_line_uvolts(wm8350); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static enum power_supply_property wm8350_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +/********************************************************************* + * USB Power + *********************************************************************/ +static int wm8350_usb_get_prop(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm8350 *wm8350 = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = !!(wm8350_get_supplies(wm8350) & + WM8350_USB_SUPPLY); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = wm8350_read_usb_uvolts(wm8350); + break; + default: + ret = -EINVAL; + break; + } + return ret; +} + +static enum power_supply_property wm8350_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +/********************************************************************* + * Battery properties + *********************************************************************/ + +static int wm8350_bat_check_health(struct wm8350 *wm8350) +{ + u16 reg; + + if (wm8350_read_battery_uvolts(wm8350) < 2850000) + return POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; + + reg = wm8350_reg_read(wm8350, WM8350_CHARGER_OVERRIDES); + if (reg & WM8350_CHG_BATT_HOT_OVRDE) + return POWER_SUPPLY_HEALTH_OVERHEAT; + + if (reg & WM8350_CHG_BATT_COLD_OVRDE) + return POWER_SUPPLY_HEALTH_COLD; + + return POWER_SUPPLY_HEALTH_GOOD; +} + +static int wm8350_bat_get_charge_type(struct wm8350 *wm8350) +{ + int state; + + state = wm8350_reg_read(wm8350, WM8350_BATTERY_CHARGER_CONTROL_2) & + WM8350_CHG_STS_MASK; + switch (state) { + case WM8350_CHG_STS_OFF: + return POWER_SUPPLY_CHARGE_TYPE_NONE; + case WM8350_CHG_STS_TRICKLE: + return POWER_SUPPLY_CHARGE_TYPE_TRICKLE; + case WM8350_CHG_STS_FAST: + return POWER_SUPPLY_CHARGE_TYPE_FAST; + default: + return POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; + } +} + +static int wm8350_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm8350 *wm8350 = dev_get_drvdata(psy->dev.parent); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = wm8350_batt_status(wm8350); + break; + case POWER_SUPPLY_PROP_ONLINE: + val->intval = !!(wm8350_get_supplies(wm8350) & + WM8350_BATT_SUPPLY); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = wm8350_read_battery_uvolts(wm8350); + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = wm8350_bat_check_health(wm8350); + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = wm8350_bat_get_charge_type(wm8350); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static enum power_supply_property wm8350_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_CHARGE_TYPE, +}; + +static const struct power_supply_desc wm8350_ac_desc = { + .name = "wm8350-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = wm8350_ac_props, + .num_properties = ARRAY_SIZE(wm8350_ac_props), + .get_property = wm8350_ac_get_prop, +}; + +static const struct power_supply_desc wm8350_battery_desc = { + .name = "wm8350-battery", + .properties = wm8350_bat_props, + .num_properties = ARRAY_SIZE(wm8350_bat_props), + .get_property = wm8350_bat_get_property, + .use_for_apm = 1, +}; + +static const struct power_supply_desc wm8350_usb_desc = { + .name = "wm8350-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = wm8350_usb_props, + .num_properties = ARRAY_SIZE(wm8350_usb_props), + .get_property = wm8350_usb_get_prop, +}; + +/********************************************************************* + * Initialisation + *********************************************************************/ + +static int wm8350_init_charger(struct wm8350 *wm8350) +{ + int ret; + + /* register our interest in charger events */ + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_BAT_HOT, + wm8350_charger_handler, 0, "Battery hot", wm8350); + if (ret) + goto err; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_BAT_COLD, + wm8350_charger_handler, 0, "Battery cold", wm8350); + if (ret) + goto free_chg_bat_hot; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_BAT_FAIL, + wm8350_charger_handler, 0, "Battery fail", wm8350); + if (ret) + goto free_chg_bat_cold; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_TO, + wm8350_charger_handler, 0, + "Charger timeout", wm8350); + if (ret) + goto free_chg_bat_fail; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_END, + wm8350_charger_handler, 0, + "Charge end", wm8350); + if (ret) + goto free_chg_to; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_START, + wm8350_charger_handler, 0, + "Charge start", wm8350); + if (ret) + goto free_chg_end; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_FAST_RDY, + wm8350_charger_handler, 0, + "Fast charge ready", wm8350); + if (ret) + goto free_chg_start; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P9, + wm8350_charger_handler, 0, + "Battery <3.9V", wm8350); + if (ret) + goto free_chg_fast_rdy; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P1, + wm8350_charger_handler, 0, + "Battery <3.1V", wm8350); + if (ret) + goto free_chg_vbatt_lt_3p9; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_2P85, + wm8350_charger_handler, 0, + "Battery <2.85V", wm8350); + if (ret) + goto free_chg_vbatt_lt_3p1; + + /* and supply change events */ + ret = wm8350_register_irq(wm8350, WM8350_IRQ_EXT_USB_FB, + wm8350_charger_handler, 0, "USB", wm8350); + if (ret) + goto free_chg_vbatt_lt_2p85; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_EXT_WALL_FB, + wm8350_charger_handler, 0, "Wall", wm8350); + if (ret) + goto free_ext_usb_fb; + + ret = wm8350_register_irq(wm8350, WM8350_IRQ_EXT_BAT_FB, + wm8350_charger_handler, 0, "Battery", wm8350); + if (ret) + goto free_ext_wall_fb; + + return 0; + +free_ext_wall_fb: + wm8350_free_irq(wm8350, WM8350_IRQ_EXT_WALL_FB, wm8350); +free_ext_usb_fb: + wm8350_free_irq(wm8350, WM8350_IRQ_EXT_USB_FB, wm8350); +free_chg_vbatt_lt_2p85: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_2P85, wm8350); +free_chg_vbatt_lt_3p1: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P1, wm8350); +free_chg_vbatt_lt_3p9: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P9, wm8350); +free_chg_fast_rdy: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_FAST_RDY, wm8350); +free_chg_start: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_START, wm8350); +free_chg_end: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_END, wm8350); +free_chg_to: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_TO, wm8350); +free_chg_bat_fail: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_FAIL, wm8350); +free_chg_bat_cold: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_COLD, wm8350); +free_chg_bat_hot: + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_HOT, wm8350); +err: + return ret; +} + +static void free_charger_irq(struct wm8350 *wm8350) +{ + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_HOT, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_COLD, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_BAT_FAIL, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_TO, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_END, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_START, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_FAST_RDY, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P9, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_3P1, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_CHG_VBATT_LT_2P85, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_EXT_USB_FB, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_EXT_WALL_FB, wm8350); + wm8350_free_irq(wm8350, WM8350_IRQ_EXT_BAT_FB, wm8350); +} + +static int wm8350_power_probe(struct platform_device *pdev) +{ + struct wm8350 *wm8350 = platform_get_drvdata(pdev); + struct wm8350_power *power = &wm8350->power; + struct wm8350_charger_policy *policy = power->policy; + int ret; + + power->ac = power_supply_register(&pdev->dev, &wm8350_ac_desc, NULL); + if (IS_ERR(power->ac)) + return PTR_ERR(power->ac); + + power->battery = power_supply_register(&pdev->dev, &wm8350_battery_desc, + NULL); + if (IS_ERR(power->battery)) { + ret = PTR_ERR(power->battery); + goto battery_failed; + } + + power->usb = power_supply_register(&pdev->dev, &wm8350_usb_desc, NULL); + if (IS_ERR(power->usb)) { + ret = PTR_ERR(power->usb); + goto usb_failed; + } + + ret = device_create_file(&pdev->dev, &dev_attr_charger_state); + if (ret < 0) + dev_warn(wm8350->dev, "failed to add charge sysfs: %d\n", ret); + ret = 0; + + wm8350_init_charger(wm8350); + if (wm8350_charger_config(wm8350, policy) == 0) { + wm8350_reg_unlock(wm8350); + wm8350_set_bits(wm8350, WM8350_POWER_MGMT_5, WM8350_CHG_ENA); + wm8350_reg_lock(wm8350); + } + + return ret; + +usb_failed: + power_supply_unregister(power->battery); +battery_failed: + power_supply_unregister(power->ac); + + return ret; +} + +static int wm8350_power_remove(struct platform_device *pdev) +{ + struct wm8350 *wm8350 = platform_get_drvdata(pdev); + struct wm8350_power *power = &wm8350->power; + + free_charger_irq(wm8350); + device_remove_file(&pdev->dev, &dev_attr_charger_state); + power_supply_unregister(power->battery); + power_supply_unregister(power->ac); + power_supply_unregister(power->usb); + return 0; +} + +static struct platform_driver wm8350_power_driver = { + .probe = wm8350_power_probe, + .remove = wm8350_power_remove, + .driver = { + .name = "wm8350-power", + }, +}; + +module_platform_driver(wm8350_power_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Power supply driver for WM8350"); +MODULE_ALIAS("platform:wm8350-power"); diff --git a/drivers/power/supply/wm97xx_battery.c b/drivers/power/supply/wm97xx_battery.c new file mode 100644 index 000000000..58f01659d --- /dev/null +++ b/drivers/power/supply/wm97xx_battery.c @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery measurement code for WM97xx + * + * based on tosa_battery.c + * + * Copyright (C) 2008 Marek Vasut <marek.vasut@gmail.com> + */ + +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/wm97xx.h> +#include <linux/spinlock.h> +#include <linux/interrupt.h> +#include <linux/gpio.h> +#include <linux/irq.h> +#include <linux/slab.h> + +static struct work_struct bat_work; +static DEFINE_MUTEX(work_lock); +static int bat_status = POWER_SUPPLY_STATUS_UNKNOWN; +static enum power_supply_property *prop; + +static unsigned long wm97xx_read_bat(struct power_supply *bat_ps) +{ + struct wm97xx_batt_pdata *pdata = power_supply_get_drvdata(bat_ps); + + return wm97xx_read_aux_adc(dev_get_drvdata(bat_ps->dev.parent), + pdata->batt_aux) * pdata->batt_mult / + pdata->batt_div; +} + +static unsigned long wm97xx_read_temp(struct power_supply *bat_ps) +{ + struct wm97xx_batt_pdata *pdata = power_supply_get_drvdata(bat_ps); + + return wm97xx_read_aux_adc(dev_get_drvdata(bat_ps->dev.parent), + pdata->temp_aux) * pdata->temp_mult / + pdata->temp_div; +} + +static int wm97xx_bat_get_property(struct power_supply *bat_ps, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct wm97xx_batt_pdata *pdata = power_supply_get_drvdata(bat_ps); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = bat_status; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = pdata->batt_tech; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (pdata->batt_aux >= 0) + val->intval = wm97xx_read_bat(bat_ps); + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_TEMP: + if (pdata->temp_aux >= 0) + val->intval = wm97xx_read_temp(bat_ps); + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + if (pdata->max_voltage >= 0) + val->intval = pdata->max_voltage; + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + if (pdata->min_voltage >= 0) + val->intval = pdata->min_voltage; + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + default: + return -EINVAL; + } + return 0; +} + +static void wm97xx_bat_external_power_changed(struct power_supply *bat_ps) +{ + schedule_work(&bat_work); +} + +static void wm97xx_bat_update(struct power_supply *bat_ps) +{ + int old_status = bat_status; + struct wm97xx_batt_pdata *pdata = power_supply_get_drvdata(bat_ps); + + mutex_lock(&work_lock); + + bat_status = (pdata->charge_gpio >= 0) ? + (gpio_get_value(pdata->charge_gpio) ? + POWER_SUPPLY_STATUS_DISCHARGING : + POWER_SUPPLY_STATUS_CHARGING) : + POWER_SUPPLY_STATUS_UNKNOWN; + + if (old_status != bat_status) { + pr_debug("%s: %i -> %i\n", bat_ps->desc->name, old_status, + bat_status); + power_supply_changed(bat_ps); + } + + mutex_unlock(&work_lock); +} + +static struct power_supply *bat_psy; +static struct power_supply_desc bat_psy_desc = { + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = wm97xx_bat_get_property, + .external_power_changed = wm97xx_bat_external_power_changed, + .use_for_apm = 1, +}; + +static void wm97xx_bat_work(struct work_struct *work) +{ + wm97xx_bat_update(bat_psy); +} + +static irqreturn_t wm97xx_chrg_irq(int irq, void *data) +{ + schedule_work(&bat_work); + return IRQ_HANDLED; +} + +#ifdef CONFIG_PM +static int wm97xx_bat_suspend(struct device *dev) +{ + flush_work(&bat_work); + return 0; +} + +static int wm97xx_bat_resume(struct device *dev) +{ + schedule_work(&bat_work); + return 0; +} + +static const struct dev_pm_ops wm97xx_bat_pm_ops = { + .suspend = wm97xx_bat_suspend, + .resume = wm97xx_bat_resume, +}; +#endif + +static int wm97xx_bat_probe(struct platform_device *dev) +{ + int ret = 0; + int props = 1; /* POWER_SUPPLY_PROP_PRESENT */ + int i = 0; + struct wm97xx_batt_pdata *pdata = dev->dev.platform_data; + struct power_supply_config cfg = {}; + + if (!pdata) { + dev_err(&dev->dev, "No platform data supplied\n"); + return -EINVAL; + } + + cfg.drv_data = pdata; + + if (dev->id != -1) + return -EINVAL; + + if (gpio_is_valid(pdata->charge_gpio)) { + ret = gpio_request(pdata->charge_gpio, "BATT CHRG"); + if (ret) + goto err; + ret = gpio_direction_input(pdata->charge_gpio); + if (ret) + goto err2; + ret = request_irq(gpio_to_irq(pdata->charge_gpio), + wm97xx_chrg_irq, 0, + "AC Detect", dev); + if (ret) + goto err2; + props++; /* POWER_SUPPLY_PROP_STATUS */ + } + + if (pdata->batt_tech >= 0) + props++; /* POWER_SUPPLY_PROP_TECHNOLOGY */ + if (pdata->temp_aux >= 0) + props++; /* POWER_SUPPLY_PROP_TEMP */ + if (pdata->batt_aux >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_NOW */ + if (pdata->max_voltage >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_MAX */ + if (pdata->min_voltage >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_MIN */ + + prop = kcalloc(props, sizeof(*prop), GFP_KERNEL); + if (!prop) { + ret = -ENOMEM; + goto err3; + } + + prop[i++] = POWER_SUPPLY_PROP_PRESENT; + if (pdata->charge_gpio >= 0) + prop[i++] = POWER_SUPPLY_PROP_STATUS; + if (pdata->batt_tech >= 0) + prop[i++] = POWER_SUPPLY_PROP_TECHNOLOGY; + if (pdata->temp_aux >= 0) + prop[i++] = POWER_SUPPLY_PROP_TEMP; + if (pdata->batt_aux >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_NOW; + if (pdata->max_voltage >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_MAX; + if (pdata->min_voltage >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_MIN; + + INIT_WORK(&bat_work, wm97xx_bat_work); + + if (!pdata->batt_name) { + dev_info(&dev->dev, "Please consider setting proper battery " + "name in platform definition file, falling " + "back to name \"wm97xx-batt\"\n"); + bat_psy_desc.name = "wm97xx-batt"; + } else + bat_psy_desc.name = pdata->batt_name; + + bat_psy_desc.properties = prop; + bat_psy_desc.num_properties = props; + + bat_psy = power_supply_register(&dev->dev, &bat_psy_desc, &cfg); + if (!IS_ERR(bat_psy)) { + schedule_work(&bat_work); + } else { + ret = PTR_ERR(bat_psy); + goto err4; + } + + return 0; +err4: + kfree(prop); +err3: + if (gpio_is_valid(pdata->charge_gpio)) + free_irq(gpio_to_irq(pdata->charge_gpio), dev); +err2: + if (gpio_is_valid(pdata->charge_gpio)) + gpio_free(pdata->charge_gpio); +err: + return ret; +} + +static int wm97xx_bat_remove(struct platform_device *dev) +{ + struct wm97xx_batt_pdata *pdata = dev->dev.platform_data; + + if (pdata && gpio_is_valid(pdata->charge_gpio)) { + free_irq(gpio_to_irq(pdata->charge_gpio), dev); + gpio_free(pdata->charge_gpio); + } + cancel_work_sync(&bat_work); + power_supply_unregister(bat_psy); + kfree(prop); + return 0; +} + +static struct platform_driver wm97xx_bat_driver = { + .driver = { + .name = "wm97xx-battery", +#ifdef CONFIG_PM + .pm = &wm97xx_bat_pm_ops, +#endif + }, + .probe = wm97xx_bat_probe, + .remove = wm97xx_bat_remove, +}; + +module_platform_driver(wm97xx_bat_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Marek Vasut <marek.vasut@gmail.com>"); +MODULE_DESCRIPTION("WM97xx battery driver"); diff --git a/drivers/power/supply/z2_battery.c b/drivers/power/supply/z2_battery.c new file mode 100644 index 000000000..ebd2e42a4 --- /dev/null +++ b/drivers/power/supply/z2_battery.c @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery measurement code for Zipit Z2 + * + * Copyright (C) 2009 Peter Edwards <sweetlilmre@gmail.com> + */ + +#include <linux/module.h> +#include <linux/gpio.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/irq.h> +#include <linux/power_supply.h> +#include <linux/slab.h> +#include <linux/z2_battery.h> + +#define Z2_DEFAULT_NAME "Z2" + +struct z2_charger { + struct z2_battery_info *info; + int bat_status; + struct i2c_client *client; + struct power_supply *batt_ps; + struct power_supply_desc batt_ps_desc; + struct mutex work_lock; + struct work_struct bat_work; +}; + +static unsigned long z2_read_bat(struct z2_charger *charger) +{ + int data; + data = i2c_smbus_read_byte_data(charger->client, + charger->info->batt_I2C_reg); + if (data < 0) + return 0; + + return data * charger->info->batt_mult / charger->info->batt_div; +} + +static int z2_batt_get_property(struct power_supply *batt_ps, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct z2_charger *charger = power_supply_get_drvdata(batt_ps); + struct z2_battery_info *info = charger->info; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = charger->bat_status; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = info->batt_tech; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + if (info->batt_I2C_reg >= 0) + val->intval = z2_read_bat(charger); + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + if (info->max_voltage >= 0) + val->intval = info->max_voltage; + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + if (info->min_voltage >= 0) + val->intval = info->min_voltage; + else + return -EINVAL; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + default: + return -EINVAL; + } + + return 0; +} + +static void z2_batt_ext_power_changed(struct power_supply *batt_ps) +{ + struct z2_charger *charger = power_supply_get_drvdata(batt_ps); + + schedule_work(&charger->bat_work); +} + +static void z2_batt_update(struct z2_charger *charger) +{ + int old_status = charger->bat_status; + struct z2_battery_info *info; + + info = charger->info; + + mutex_lock(&charger->work_lock); + + charger->bat_status = (info->charge_gpio >= 0) ? + (gpio_get_value(info->charge_gpio) ? + POWER_SUPPLY_STATUS_CHARGING : + POWER_SUPPLY_STATUS_DISCHARGING) : + POWER_SUPPLY_STATUS_UNKNOWN; + + if (old_status != charger->bat_status) { + pr_debug("%s: %i -> %i\n", charger->batt_ps->desc->name, + old_status, + charger->bat_status); + power_supply_changed(charger->batt_ps); + } + + mutex_unlock(&charger->work_lock); +} + +static void z2_batt_work(struct work_struct *work) +{ + struct z2_charger *charger; + charger = container_of(work, struct z2_charger, bat_work); + z2_batt_update(charger); +} + +static irqreturn_t z2_charge_switch_irq(int irq, void *devid) +{ + struct z2_charger *charger = devid; + schedule_work(&charger->bat_work); + return IRQ_HANDLED; +} + +static int z2_batt_ps_init(struct z2_charger *charger, int props) +{ + int i = 0; + enum power_supply_property *prop; + struct z2_battery_info *info = charger->info; + + if (info->charge_gpio >= 0) + props++; /* POWER_SUPPLY_PROP_STATUS */ + if (info->batt_tech >= 0) + props++; /* POWER_SUPPLY_PROP_TECHNOLOGY */ + if (info->batt_I2C_reg >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_NOW */ + if (info->max_voltage >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_MAX */ + if (info->min_voltage >= 0) + props++; /* POWER_SUPPLY_PROP_VOLTAGE_MIN */ + + prop = kcalloc(props, sizeof(*prop), GFP_KERNEL); + if (!prop) + return -ENOMEM; + + prop[i++] = POWER_SUPPLY_PROP_PRESENT; + if (info->charge_gpio >= 0) + prop[i++] = POWER_SUPPLY_PROP_STATUS; + if (info->batt_tech >= 0) + prop[i++] = POWER_SUPPLY_PROP_TECHNOLOGY; + if (info->batt_I2C_reg >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_NOW; + if (info->max_voltage >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_MAX; + if (info->min_voltage >= 0) + prop[i++] = POWER_SUPPLY_PROP_VOLTAGE_MIN; + + if (!info->batt_name) { + dev_info(&charger->client->dev, + "Please consider setting proper battery " + "name in platform definition file, falling " + "back to name \" Z2_DEFAULT_NAME \"\n"); + charger->batt_ps_desc.name = Z2_DEFAULT_NAME; + } else + charger->batt_ps_desc.name = info->batt_name; + + charger->batt_ps_desc.properties = prop; + charger->batt_ps_desc.num_properties = props; + charger->batt_ps_desc.type = POWER_SUPPLY_TYPE_BATTERY; + charger->batt_ps_desc.get_property = z2_batt_get_property; + charger->batt_ps_desc.external_power_changed = + z2_batt_ext_power_changed; + charger->batt_ps_desc.use_for_apm = 1; + + return 0; +} + +static int z2_batt_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret = 0; + int props = 1; /* POWER_SUPPLY_PROP_PRESENT */ + struct z2_charger *charger; + struct z2_battery_info *info = client->dev.platform_data; + struct power_supply_config psy_cfg = {}; + + if (info == NULL) { + dev_err(&client->dev, + "Please set platform device platform_data" + " to a valid z2_battery_info pointer!\n"); + return -EINVAL; + } + + charger = kzalloc(sizeof(*charger), GFP_KERNEL); + if (charger == NULL) + return -ENOMEM; + + charger->bat_status = POWER_SUPPLY_STATUS_UNKNOWN; + charger->info = info; + charger->client = client; + i2c_set_clientdata(client, charger); + psy_cfg.drv_data = charger; + + mutex_init(&charger->work_lock); + + if (info->charge_gpio >= 0 && gpio_is_valid(info->charge_gpio)) { + ret = gpio_request(info->charge_gpio, "BATT CHRG"); + if (ret) + goto err; + + ret = gpio_direction_input(info->charge_gpio); + if (ret) + goto err2; + + irq_set_irq_type(gpio_to_irq(info->charge_gpio), + IRQ_TYPE_EDGE_BOTH); + ret = request_irq(gpio_to_irq(info->charge_gpio), + z2_charge_switch_irq, 0, + "AC Detect", charger); + if (ret) + goto err3; + } + + ret = z2_batt_ps_init(charger, props); + if (ret) + goto err3; + + INIT_WORK(&charger->bat_work, z2_batt_work); + + charger->batt_ps = power_supply_register(&client->dev, + &charger->batt_ps_desc, + &psy_cfg); + if (IS_ERR(charger->batt_ps)) { + ret = PTR_ERR(charger->batt_ps); + goto err4; + } + + schedule_work(&charger->bat_work); + + return 0; + +err4: + kfree(charger->batt_ps_desc.properties); +err3: + if (info->charge_gpio >= 0 && gpio_is_valid(info->charge_gpio)) + free_irq(gpio_to_irq(info->charge_gpio), charger); +err2: + if (info->charge_gpio >= 0 && gpio_is_valid(info->charge_gpio)) + gpio_free(info->charge_gpio); +err: + kfree(charger); + return ret; +} + +static int z2_batt_remove(struct i2c_client *client) +{ + struct z2_charger *charger = i2c_get_clientdata(client); + struct z2_battery_info *info = charger->info; + + cancel_work_sync(&charger->bat_work); + power_supply_unregister(charger->batt_ps); + + kfree(charger->batt_ps_desc.properties); + if (info->charge_gpio >= 0 && gpio_is_valid(info->charge_gpio)) { + free_irq(gpio_to_irq(info->charge_gpio), charger); + gpio_free(info->charge_gpio); + } + + kfree(charger); + + return 0; +} + +#ifdef CONFIG_PM +static int z2_batt_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct z2_charger *charger = i2c_get_clientdata(client); + + flush_work(&charger->bat_work); + return 0; +} + +static int z2_batt_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct z2_charger *charger = i2c_get_clientdata(client); + + schedule_work(&charger->bat_work); + return 0; +} + +static const struct dev_pm_ops z2_battery_pm_ops = { + .suspend = z2_batt_suspend, + .resume = z2_batt_resume, +}; + +#define Z2_BATTERY_PM_OPS (&z2_battery_pm_ops) + +#else +#define Z2_BATTERY_PM_OPS (NULL) +#endif + +static const struct i2c_device_id z2_batt_id[] = { + { "aer915", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, z2_batt_id); + +static struct i2c_driver z2_batt_driver = { + .driver = { + .name = "z2-battery", + .pm = Z2_BATTERY_PM_OPS + }, + .probe = z2_batt_probe, + .remove = z2_batt_remove, + .id_table = z2_batt_id, +}; +module_i2c_driver(z2_batt_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Peter Edwards <sweetlilmre@gmail.com>"); +MODULE_DESCRIPTION("Zipit Z2 battery driver"); |