| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (c) 2023, Nikita Travkin <nikita@trvn.ru> | |
| */ | |
| #include <linux/errno.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/power_supply.h> | |
| #include <linux/property.h> | |
| #include <linux/regmap.h> | |
| #include <linux/slab.h> | |
| #include <linux/delay.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/extcon-provider.h> | |
| /* Two bytes: type + subtype */ | |
| #define PM8916_PERPH_TYPE 0x04 | |
| #define PM8916_LBC_CHGR_TYPE 0x1502 | |
| #define PM8916_LBC_BAT_IF_TYPE 0x1602 | |
| #define PM8916_LBC_USB_TYPE 0x1702 | |
| #define PM8916_LBC_MISC_TYPE 0x1802 | |
| #define PM8916_LBC_CHGR_CHG_OPTION 0x08 | |
| #define PM8916_LBC_CHGR_PMIC_CHARGER BIT(7) | |
| #define PM8916_LBC_CHGR_CHG_STATUS 0x09 | |
| #define PM8916_INT_RT_STS 0x10 | |
| #define PM8916_LBC_USB_USBIN_VALID BIT(1) | |
| #define PM8916_LBC_CHGR_VDD_MAX 0x40 | |
| #define PM8916_LBC_CHGR_VDD_SAFE 0x41 | |
| #define PM8916_LBC_CHGR_IBAT_MAX 0x44 | |
| #define PM8916_LBC_CHGR_IBAT_SAFE 0x45 | |
| #define PM8916_LBC_CHGR_TCHG_MAX_EN 0x60 | |
| #define PM8916_LBC_CHGR_TCHG_MAX_ENABLED BIT(7) | |
| #define PM8916_LBC_CHGR_TCHG_MAX 0x61 | |
| #define PM8916_LBC_CHGR_CHG_CTRL 0x49 | |
| #define PM8916_LBC_CHGR_CHG_EN BIT(7) | |
| #define PM8916_LBC_CHGR_PSTG_EN BIT(5) | |
| #define PM8916_LBC_CHGR_MIN_CURRENT 90000 | |
| #define PM8916_LBC_CHGR_MAX_CURRENT 1440000 | |
| #define PM8916_LBC_CHGR_MIN_VOLTAGE 4000000 | |
| #define PM8916_LBC_CHGR_MAX_VOLTAGE 4775000 | |
| #define PM8916_LBC_CHGR_VOLTAGE_STEP 25000 | |
| #define PM8916_LBC_CHGR_MIN_TIME 4 | |
| #define PM8916_LBC_CHGR_MAX_TIME 256 | |
| struct pm8916_lbc_charger { | |
| struct device *dev; | |
| struct extcon_dev *edev; | |
| struct power_supply *charger; | |
| struct power_supply_battery_info *info; | |
| struct regmap *regmap; | |
| unsigned int reg[4]; | |
| bool online; | |
| unsigned int charge_voltage_max; | |
| unsigned int charge_voltage_safe; | |
| unsigned int charge_current_max; | |
| unsigned int charge_current_safe; | |
| }; | |
| static const unsigned int pm8916_lbc_charger_cable[] = { | |
| EXTCON_USB, | |
| EXTCON_NONE, | |
| }; | |
| enum { | |
| LBC_CHGR = 0, | |
| LBC_BAT_IF, | |
| LBC_USB, | |
| LBC_MISC, | |
| }; | |
| static int pm8916_lbc_charger_configure(struct pm8916_lbc_charger *chg) | |
| { | |
| int ret = 0; | |
| unsigned int tmp; | |
| chg->charge_voltage_max = clamp_t(u32, chg->charge_voltage_max, | |
| PM8916_LBC_CHGR_MIN_VOLTAGE, chg->charge_voltage_safe); | |
| tmp = chg->charge_voltage_max - PM8916_LBC_CHGR_MIN_VOLTAGE; | |
| tmp /= PM8916_LBC_CHGR_VOLTAGE_STEP; | |
| chg->charge_voltage_max = PM8916_LBC_CHGR_MIN_VOLTAGE + tmp * PM8916_LBC_CHGR_VOLTAGE_STEP; | |
| ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_VDD_MAX, tmp); | |
| if (ret) | |
| goto error; | |
| chg->charge_current_max = min(chg->charge_current_max, chg->charge_current_safe); | |
| tmp = clamp_t(u32, chg->charge_current_max, | |
| PM8916_LBC_CHGR_MIN_CURRENT, PM8916_LBC_CHGR_MAX_CURRENT); | |
| tmp = chg->charge_current_max / PM8916_LBC_CHGR_MIN_CURRENT - 1; | |
| chg->charge_current_max = (tmp + 1) * PM8916_LBC_CHGR_MIN_CURRENT; | |
| ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_IBAT_MAX, tmp); | |
| if (ret) | |
| goto error; | |
| ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_CHG_CTRL, | |
| PM8916_LBC_CHGR_CHG_EN | PM8916_LBC_CHGR_PSTG_EN); | |
| if (ret) | |
| goto error; | |
| return ret; | |
| error: | |
| dev_err(chg->dev, "Failed to configure charging: %pe\n", ERR_PTR(ret)); | |
| return ret; | |
| } | |
| static int pm8916_lbc_charger_get_property(struct power_supply *psy, | |
| enum power_supply_property psp, | |
| union power_supply_propval *val) | |
| { | |
| struct pm8916_lbc_charger *chg = power_supply_get_drvdata(psy); | |
| switch (psp) { | |
| case POWER_SUPPLY_PROP_ONLINE: | |
| val->intval = chg->online; | |
| return 0; | |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: | |
| val->intval = chg->charge_voltage_max; | |
| return 0; | |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: | |
| val->intval = chg->charge_current_max; | |
| return 0; | |
| default: | |
| return -EINVAL; | |
| }; | |
| } | |
| static int pm8916_lbc_charger_set_property(struct power_supply *psy, | |
| enum power_supply_property prop, | |
| const union power_supply_propval *val) | |
| { | |
| struct pm8916_lbc_charger *chg = power_supply_get_drvdata(psy); | |
| switch (prop) { | |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: | |
| chg->charge_current_max = val->intval; | |
| return pm8916_lbc_charger_configure(chg); | |
| default: | |
| return -EINVAL; | |
| } | |
| } | |
| static int pm8916_lbc_charger_property_is_writeable(struct power_supply *psy, | |
| enum power_supply_property psp) | |
| { | |
| switch (psp) { | |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| static enum power_supply_property pm8916_lbc_charger_properties[] = { | |
| POWER_SUPPLY_PROP_ONLINE, | |
| POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, | |
| POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, | |
| }; | |
| static irqreturn_t pm8916_lbc_charger_state_changed_irq(int irq, void *data) | |
| { | |
| struct pm8916_lbc_charger *chg = data; | |
| unsigned int tmp; | |
| int ret; | |
| ret = regmap_read(chg->regmap, chg->reg[LBC_USB] + PM8916_INT_RT_STS, &tmp); | |
| if (ret) | |
| return IRQ_HANDLED; | |
| chg->online = !!(tmp & PM8916_LBC_USB_USBIN_VALID); | |
| extcon_set_state_sync(chg->edev, EXTCON_USB, chg->online); | |
| power_supply_changed(chg->charger); | |
| return IRQ_HANDLED; | |
| } | |
| static int pm8916_lbc_charger_probe_dt(struct pm8916_lbc_charger *chg) | |
| { | |
| struct device *dev = chg->dev; | |
| int ret = 0; | |
| unsigned int tmp; | |
| ret = device_property_read_u32(dev, "qcom,fast-charge-safe-voltage", &chg->charge_voltage_safe); | |
| if (ret) | |
| return ret; | |
| if (chg->charge_voltage_safe < PM8916_LBC_CHGR_MIN_VOLTAGE) | |
| return -EINVAL; | |
| chg->charge_voltage_safe = clamp_t(u32, chg->charge_voltage_safe, | |
| PM8916_LBC_CHGR_MIN_VOLTAGE, PM8916_LBC_CHGR_MAX_VOLTAGE); | |
| tmp = chg->charge_voltage_safe - PM8916_LBC_CHGR_MIN_VOLTAGE; | |
| tmp /= PM8916_LBC_CHGR_VOLTAGE_STEP; | |
| ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_VDD_SAFE, tmp); | |
| if (ret) | |
| return ret; | |
| ret = device_property_read_u32(dev, "qcom,fast-charge-safe-current", &chg->charge_current_safe); | |
| if (ret) | |
| return ret; | |
| if (chg->charge_current_safe < PM8916_LBC_CHGR_MIN_CURRENT) | |
| return -EINVAL; | |
| chg->charge_current_safe = clamp_t(u32, chg->charge_current_safe, | |
| PM8916_LBC_CHGR_MIN_CURRENT, PM8916_LBC_CHGR_MAX_CURRENT); | |
| chg->charge_current_max = chg->charge_current_safe; | |
| tmp = chg->charge_current_safe / PM8916_LBC_CHGR_MIN_CURRENT - 1; | |
| ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_IBAT_SAFE, tmp); | |
| if (ret) | |
| return ret; | |
| /* Disable charger timeout. */ | |
| return regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_TCHG_MAX_EN, 0x00); | |
| } | |
| static const struct power_supply_desc pm8916_lbc_charger_psy_desc = { | |
| .name = "pm8916-lbc-chgr", | |
| .type = POWER_SUPPLY_TYPE_USB, | |
| .properties = pm8916_lbc_charger_properties, | |
| .num_properties = ARRAY_SIZE(pm8916_lbc_charger_properties), | |
| .get_property = pm8916_lbc_charger_get_property, | |
| .set_property = pm8916_lbc_charger_set_property, | |
| .property_is_writeable = pm8916_lbc_charger_property_is_writeable, | |
| }; | |
| static int pm8916_lbc_charger_probe(struct platform_device *pdev) | |
| { | |
| struct device *dev = &pdev->dev; | |
| struct pm8916_lbc_charger *chg; | |
| struct power_supply_config psy_cfg = {}; | |
| int ret, len, irq; | |
| unsigned int tmp; | |
| chg = devm_kzalloc(dev, sizeof(*chg), GFP_KERNEL); | |
| if (!chg) | |
| return -ENOMEM; | |
| chg->dev = dev; | |
| chg->regmap = dev_get_regmap(pdev->dev.parent, NULL); | |
| if (!chg->regmap) | |
| return -ENODEV; | |
| len = device_property_count_u32(dev, "reg"); | |
| if (len < 0) | |
| return len; | |
| if (len != 4) | |
| return dev_err_probe(dev, -EINVAL, | |
| "Wrong amount of reg values: %d (4 expected)\n", len); | |
| ret = device_property_read_u32_array(dev, "reg", chg->reg, len); | |
| if (ret) | |
| return ret; | |
| ret = regmap_bulk_read(chg->regmap, chg->reg[LBC_CHGR] + PM8916_PERPH_TYPE, &tmp, 2); | |
| if (ret) | |
| goto comm_error; | |
| if (tmp != PM8916_LBC_CHGR_TYPE) | |
| goto type_error; | |
| ret = regmap_bulk_read(chg->regmap, chg->reg[LBC_BAT_IF] + PM8916_PERPH_TYPE, &tmp, 2); | |
| if (ret) | |
| goto comm_error; | |
| if (tmp != PM8916_LBC_BAT_IF_TYPE) | |
| goto type_error; | |
| ret = regmap_bulk_read(chg->regmap, chg->reg[LBC_USB] + PM8916_PERPH_TYPE, &tmp, 2); | |
| if (ret) | |
| goto comm_error; | |
| if (tmp != PM8916_LBC_USB_TYPE) | |
| goto type_error; | |
| ret = regmap_bulk_read(chg->regmap, chg->reg[LBC_MISC] + PM8916_PERPH_TYPE, &tmp, 2); | |
| if (ret) | |
| goto comm_error; | |
| if (tmp != PM8916_LBC_MISC_TYPE) | |
| goto type_error; | |
| ret = regmap_read(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_CHG_OPTION, &tmp); | |
| if (ret) | |
| goto comm_error; | |
| if (tmp != PM8916_LBC_CHGR_PMIC_CHARGER) | |
| dev_err_probe(dev, -ENODEV, "The system is using an external charger\n"); | |
| ret = pm8916_lbc_charger_probe_dt(chg); | |
| if (ret) | |
| dev_err_probe(dev, ret, "Error while parsing device tree\n"); | |
| psy_cfg.drv_data = chg; | |
| psy_cfg.fwnode = dev_fwnode(dev); | |
| chg->charger = devm_power_supply_register(dev, &pm8916_lbc_charger_psy_desc, &psy_cfg); | |
| if (IS_ERR(chg->charger)) | |
| return dev_err_probe(dev, PTR_ERR(chg->charger), "Unable to register charger\n"); | |
| ret = power_supply_get_battery_info(chg->charger, &chg->info); | |
| if (ret) | |
| return dev_err_probe(dev, ret, "Unable to get battery info\n"); | |
| irq = platform_get_irq_byname(pdev, "usb_vbus"); | |
| if (irq < 0) | |
| return irq; | |
| chg->edev = devm_extcon_dev_allocate(dev, pm8916_lbc_charger_cable); | |
| if (IS_ERR(chg->edev)) | |
| return PTR_ERR(chg->edev); | |
| ret = devm_extcon_dev_register(dev, chg->edev); | |
| if (ret < 0) | |
| return dev_err_probe(dev, ret, "failed to register extcon device\n"); | |
| ret = devm_request_threaded_irq(dev, irq, NULL, pm8916_lbc_charger_state_changed_irq, | |
| IRQF_ONESHOT, "pm8916_lbc", chg); | |
| if (ret) | |
| return ret; | |
| ret = regmap_read(chg->regmap, chg->reg[LBC_USB] + PM8916_INT_RT_STS, &tmp); | |
| if (ret) | |
| goto comm_error; | |
| chg->online = !!(tmp & PM8916_LBC_USB_USBIN_VALID); | |
| extcon_set_state_sync(chg->edev, EXTCON_USB, chg->online); | |
| chg->charge_voltage_max = chg->info->voltage_max_design_uv; | |
| ret = pm8916_lbc_charger_configure(chg); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| comm_error: | |
| return dev_err_probe(dev, ret, "Unable to communicate with device\n"); | |
| type_error: | |
| return dev_err_probe(dev, -ENODEV, "Device reported wrong type: 0x%X\n", tmp); | |
| } | |
| static const struct of_device_id pm8916_lbc_charger_of_match[] = { | |
| { .compatible = "qcom,pm8916-lbc", }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(of, pm8916_lbc_charger_of_match); | |
| static struct platform_driver pm8916_lbc_charger_driver = { | |
| .driver = { | |
| .name = "pm8916-lbc", | |
| .of_match_table = pm8916_lbc_charger_of_match, | |
| }, | |
| .probe = pm8916_lbc_charger_probe, | |
| }; | |
| module_platform_driver(pm8916_lbc_charger_driver); | |
| MODULE_DESCRIPTION("pm8916 LBC driver"); | |
| MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>"); | |
| MODULE_LICENSE("GPL"); |