// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2023, Nikita Travkin */ #include #include #include #include #include #include #include #include #include #include #include /* 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. */ ret = regmap_write(chg->regmap, chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_TCHG_MAX_EN, 0x00); if (ret) return ret; return ret; } 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); irq = platform_get_irq_byname(pdev, "usb_vbus"); if (irq < 0) return irq; ret = devm_request_threaded_irq(dev, irq, NULL, pm8916_lbc_charger_state_changed_irq, IRQF_ONESHOT, "pm8916_lbc", chg); if (ret) return ret; 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.of_node = dev->of_node; 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"); 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 = 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 "); MODULE_LICENSE("GPL");