// SPDX-License-Identifier: GPL-2.0+ /* * max77976_charger.c - Driver for the Maxim MAX77976 battery charger * * Copyright (C) 2021 Luca Ceresoli * Author: Luca Ceresoli */ #include #include #include #include #define MAX77976_DRIVER_NAME "max77976-charger" #define MAX77976_CHIP_ID 0x76 static const char *max77976_manufacturer = "Maxim Integrated"; static const char *max77976_model = "MAX77976"; /* -------------------------------------------------------------------------- * Register map */ #define MAX77976_REG_CHIP_ID 0x00 #define MAX77976_REG_CHIP_REVISION 0x01 #define MAX77976_REG_CHG_INT_OK 0x12 #define MAX77976_REG_CHG_DETAILS_01 0x14 #define MAX77976_REG_CHG_CNFG_00 0x16 #define MAX77976_REG_CHG_CNFG_02 0x18 #define MAX77976_REG_CHG_CNFG_06 0x1c #define MAX77976_REG_CHG_CNFG_09 0x1f /* CHG_DETAILS_01.CHG_DTLS values */ enum max77976_charging_state { MAX77976_CHARGING_PREQUALIFICATION = 0x0, MAX77976_CHARGING_FAST_CONST_CURRENT, MAX77976_CHARGING_FAST_CONST_VOLTAGE, MAX77976_CHARGING_TOP_OFF, MAX77976_CHARGING_DONE, MAX77976_CHARGING_RESERVED_05, MAX77976_CHARGING_TIMER_FAULT, MAX77976_CHARGING_SUSPENDED_QBATT_OFF, MAX77976_CHARGING_OFF, MAX77976_CHARGING_RESERVED_09, MAX77976_CHARGING_THERMAL_SHUTDOWN, MAX77976_CHARGING_WATCHDOG_EXPIRED, MAX77976_CHARGING_SUSPENDED_JEITA, MAX77976_CHARGING_SUSPENDED_THM_REMOVAL, MAX77976_CHARGING_SUSPENDED_PIN, MAX77976_CHARGING_RESERVED_0F, }; /* CHG_DETAILS_01.BAT_DTLS values */ enum max77976_battery_state { MAX77976_BATTERY_BATTERY_REMOVAL = 0x0, MAX77976_BATTERY_PREQUALIFICATION, MAX77976_BATTERY_TIMER_FAULT, MAX77976_BATTERY_REGULAR_VOLTAGE, MAX77976_BATTERY_LOW_VOLTAGE, MAX77976_BATTERY_OVERVOLTAGE, MAX77976_BATTERY_RESERVED, MAX77976_BATTERY_BATTERY_ONLY, // No valid adapter is present }; /* CHG_CNFG_00.MODE values */ enum max77976_mode { MAX77976_MODE_CHARGER_BUCK = 0x5, MAX77976_MODE_BOOST = 0x9, }; /* CHG_CNFG_02.CHG_CC: charge current limit, 100..5500 mA, 50 mA steps */ #define MAX77976_CHG_CC_STEP 50000U #define MAX77976_CHG_CC_MIN 100000U #define MAX77976_CHG_CC_MAX 5500000U /* CHG_CNFG_09.CHGIN_ILIM: input current limit, 100..3200 mA, 100 mA steps */ #define MAX77976_CHGIN_ILIM_STEP 100000U #define MAX77976_CHGIN_ILIM_MIN 100000U #define MAX77976_CHGIN_ILIM_MAX 3200000U enum max77976_field_idx { VERSION, REVISION, /* CHIP_REVISION */ CHGIN_OK, /* CHG_INT_OK */ BAT_DTLS, CHG_DTLS, /* CHG_DETAILS_01 */ MODE, /* CHG_CNFG_00 */ CHG_CC, /* CHG_CNFG_02 */ CHGPROT, /* CHG_CNFG_06 */ CHGIN_ILIM, /* CHG_CNFG_09 */ MAX77976_N_REGMAP_FIELDS }; static const struct reg_field max77976_reg_field[MAX77976_N_REGMAP_FIELDS] = { [VERSION] = REG_FIELD(MAX77976_REG_CHIP_REVISION, 4, 7), [REVISION] = REG_FIELD(MAX77976_REG_CHIP_REVISION, 0, 3), [CHGIN_OK] = REG_FIELD(MAX77976_REG_CHG_INT_OK, 6, 6), [CHG_DTLS] = REG_FIELD(MAX77976_REG_CHG_DETAILS_01, 0, 3), [BAT_DTLS] = REG_FIELD(MAX77976_REG_CHG_DETAILS_01, 4, 6), [MODE] = REG_FIELD(MAX77976_REG_CHG_CNFG_00, 0, 3), [CHG_CC] = REG_FIELD(MAX77976_REG_CHG_CNFG_02, 0, 6), [CHGPROT] = REG_FIELD(MAX77976_REG_CHG_CNFG_06, 2, 3), [CHGIN_ILIM] = REG_FIELD(MAX77976_REG_CHG_CNFG_09, 0, 5), }; static const struct regmap_config max77976_regmap_config = { .reg_bits = 8, .val_bits = 8, .max_register = 0x24, }; /* -------------------------------------------------------------------------- * Data structures */ struct max77976 { struct i2c_client *client; struct regmap *regmap; struct regmap_field *rfield[MAX77976_N_REGMAP_FIELDS]; }; /* -------------------------------------------------------------------------- * power_supply properties */ static int max77976_get_status(struct max77976 *chg, int *val) { unsigned int regval; int err; err = regmap_field_read(chg->rfield[CHG_DTLS], ®val); if (err < 0) return err; switch (regval) { case MAX77976_CHARGING_PREQUALIFICATION: case MAX77976_CHARGING_FAST_CONST_CURRENT: case MAX77976_CHARGING_FAST_CONST_VOLTAGE: case MAX77976_CHARGING_TOP_OFF: *val = POWER_SUPPLY_STATUS_CHARGING; break; case MAX77976_CHARGING_DONE: *val = POWER_SUPPLY_STATUS_FULL; break; case MAX77976_CHARGING_TIMER_FAULT: case MAX77976_CHARGING_SUSPENDED_QBATT_OFF: case MAX77976_CHARGING_SUSPENDED_JEITA: case MAX77976_CHARGING_SUSPENDED_THM_REMOVAL: case MAX77976_CHARGING_SUSPENDED_PIN: *val = POWER_SUPPLY_STATUS_NOT_CHARGING; break; case MAX77976_CHARGING_OFF: case MAX77976_CHARGING_THERMAL_SHUTDOWN: case MAX77976_CHARGING_WATCHDOG_EXPIRED: *val = POWER_SUPPLY_STATUS_DISCHARGING; break; default: *val = POWER_SUPPLY_STATUS_UNKNOWN; } return 0; } static int max77976_get_charge_type(struct max77976 *chg, int *val) { unsigned int regval; int err; err = regmap_field_read(chg->rfield[CHG_DTLS], ®val); if (err < 0) return err; switch (regval) { case MAX77976_CHARGING_PREQUALIFICATION: *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; break; case MAX77976_CHARGING_FAST_CONST_CURRENT: case MAX77976_CHARGING_FAST_CONST_VOLTAGE: *val = POWER_SUPPLY_CHARGE_TYPE_FAST; break; case MAX77976_CHARGING_TOP_OFF: *val = POWER_SUPPLY_CHARGE_TYPE_STANDARD; break; case MAX77976_CHARGING_DONE: case MAX77976_CHARGING_TIMER_FAULT: case MAX77976_CHARGING_SUSPENDED_QBATT_OFF: case MAX77976_CHARGING_OFF: case MAX77976_CHARGING_THERMAL_SHUTDOWN: case MAX77976_CHARGING_WATCHDOG_EXPIRED: case MAX77976_CHARGING_SUSPENDED_JEITA: case MAX77976_CHARGING_SUSPENDED_THM_REMOVAL: case MAX77976_CHARGING_SUSPENDED_PIN: *val = POWER_SUPPLY_CHARGE_TYPE_NONE; break; default: *val = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; } return 0; } static int max77976_get_health(struct max77976 *chg, int *val) { unsigned int regval; int err; err = regmap_field_read(chg->rfield[BAT_DTLS], ®val); if (err < 0) return err; switch (regval) { case MAX77976_BATTERY_BATTERY_REMOVAL: *val = POWER_SUPPLY_HEALTH_NO_BATTERY; break; case MAX77976_BATTERY_LOW_VOLTAGE: case MAX77976_BATTERY_REGULAR_VOLTAGE: *val = POWER_SUPPLY_HEALTH_GOOD; break; case MAX77976_BATTERY_TIMER_FAULT: *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; break; case MAX77976_BATTERY_OVERVOLTAGE: *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; break; case MAX77976_BATTERY_PREQUALIFICATION: case MAX77976_BATTERY_BATTERY_ONLY: *val = POWER_SUPPLY_HEALTH_UNKNOWN; break; default: *val = POWER_SUPPLY_HEALTH_UNKNOWN; } return 0; } static int max77976_get_online(struct max77976 *chg, int *val) { unsigned int regval; int err; err = regmap_field_read(chg->rfield[CHGIN_OK], ®val); if (err < 0) return err; *val = (regval ? 1 : 0); return 0; } static int max77976_get_integer(struct max77976 *chg, enum max77976_field_idx fidx, unsigned int clamp_min, unsigned int clamp_max, unsigned int mult, int *val) { unsigned int regval; int err; err = regmap_field_read(chg->rfield[fidx], ®val); if (err < 0) return err; *val = clamp_val(regval * mult, clamp_min, clamp_max); return 0; } static int max77976_set_integer(struct max77976 *chg, enum max77976_field_idx fidx, unsigned int clamp_min, unsigned int clamp_max, unsigned int div, int val) { unsigned int regval; regval = clamp_val(val, clamp_min, clamp_max) / div; return regmap_field_write(chg->rfield[fidx], regval); } static int max77976_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct max77976 *chg = power_supply_get_drvdata(psy); int err = 0; switch (psp) { case POWER_SUPPLY_PROP_STATUS: err = max77976_get_status(chg, &val->intval); break; case POWER_SUPPLY_PROP_CHARGE_TYPE: err = max77976_get_charge_type(chg, &val->intval); break; case POWER_SUPPLY_PROP_HEALTH: err = max77976_get_health(chg, &val->intval); break; case POWER_SUPPLY_PROP_ONLINE: err = max77976_get_online(chg, &val->intval); break; case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX: val->intval = MAX77976_CHG_CC_MAX; break; case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: err = max77976_get_integer(chg, CHG_CC, MAX77976_CHG_CC_MIN, MAX77976_CHG_CC_MAX, MAX77976_CHG_CC_STEP, &val->intval); break; case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: err = max77976_get_integer(chg, CHGIN_ILIM, MAX77976_CHGIN_ILIM_MIN, MAX77976_CHGIN_ILIM_MAX, MAX77976_CHGIN_ILIM_STEP, &val->intval); break; case POWER_SUPPLY_PROP_MODEL_NAME: val->strval = max77976_model; break; case POWER_SUPPLY_PROP_MANUFACTURER: val->strval = max77976_manufacturer; break; default: err = -EINVAL; } return err; } static int max77976_set_property(struct power_supply *psy, enum power_supply_property psp, const union power_supply_propval *val) { struct max77976 *chg = power_supply_get_drvdata(psy); int err = 0; switch (psp) { case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: err = max77976_set_integer(chg, CHG_CC, MAX77976_CHG_CC_MIN, MAX77976_CHG_CC_MAX, MAX77976_CHG_CC_STEP, val->intval); break; case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: err = max77976_set_integer(chg, CHGIN_ILIM, MAX77976_CHGIN_ILIM_MIN, MAX77976_CHGIN_ILIM_MAX, MAX77976_CHGIN_ILIM_STEP, val->intval); break; default: err = -EINVAL; } return err; }; static int max77976_property_is_writeable(struct power_supply *psy, enum power_supply_property psp) { switch (psp) { case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: return true; default: return false; } } static enum power_supply_property max77976_psy_props[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_CHARGE_TYPE, POWER_SUPPLY_PROP_HEALTH, POWER_SUPPLY_PROP_ONLINE, POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER, }; static const struct power_supply_desc max77976_psy_desc = { .name = MAX77976_DRIVER_NAME, .type = POWER_SUPPLY_TYPE_USB, .properties = max77976_psy_props, .num_properties = ARRAY_SIZE(max77976_psy_props), .get_property = max77976_get_property, .set_property = max77976_set_property, .property_is_writeable = max77976_property_is_writeable, }; /* -------------------------------------------------------------------------- * Entry point */ static int max77976_detect(struct max77976 *chg) { struct device *dev = &chg->client->dev; unsigned int id, ver, rev; int err; err = regmap_read(chg->regmap, MAX77976_REG_CHIP_ID, &id); if (err) return dev_err_probe(dev, err, "cannot read chip ID\n"); if (id != MAX77976_CHIP_ID) return dev_err_probe(dev, -ENXIO, "unknown model ID 0x%02x\n", id); err = regmap_field_read(chg->rfield[VERSION], &ver); if (!err) err = regmap_field_read(chg->rfield[REVISION], &rev); if (err) return dev_err_probe(dev, -ENXIO, "cannot read version/revision\n"); dev_info(dev, "detected model MAX779%02x ver %u rev %u", id, ver, rev); return 0; } static int max77976_configure(struct max77976 *chg) { struct device *dev = &chg->client->dev; int err; /* Magic value to unlock writing to some registers */ err = regmap_field_write(chg->rfield[CHGPROT], 0x3); if (err) goto err; /* * Mode 5 = Charger ON, OTG OFF, buck ON, boost OFF. * Other modes are not implemented by this driver. */ err = regmap_field_write(chg->rfield[MODE], MAX77976_MODE_CHARGER_BUCK); if (err) goto err; return 0; err: return dev_err_probe(dev, err, "error while configuring"); } static int max77976_probe(struct i2c_client *client) { struct device *dev = &client->dev; struct power_supply_config psy_cfg = {}; struct power_supply *psy; struct max77976 *chg; int err; int i; chg = devm_kzalloc(dev, sizeof(*chg), GFP_KERNEL); if (!chg) return -ENOMEM; i2c_set_clientdata(client, chg); psy_cfg.drv_data = chg; psy_cfg.no_wakeup_source = true; chg->client = client; chg->regmap = devm_regmap_init_i2c(client, &max77976_regmap_config); if (IS_ERR(chg->regmap)) return dev_err_probe(dev, PTR_ERR(chg->regmap), "cannot allocate regmap\n"); for (i = 0; i < MAX77976_N_REGMAP_FIELDS; i++) { chg->rfield[i] = devm_regmap_field_alloc(dev, chg->regmap, max77976_reg_field[i]); if (IS_ERR(chg->rfield[i])) return dev_err_probe(dev, PTR_ERR(chg->rfield[i]), "cannot allocate regmap field\n"); } err = max77976_detect(chg); if (err) return err; err = max77976_configure(chg); if (err) return err; psy = devm_power_supply_register(dev, &max77976_psy_desc, &psy_cfg); if (IS_ERR(psy)) return dev_err_probe(dev, PTR_ERR(psy), "cannot register\n"); return 0; } static const struct i2c_device_id max77976_i2c_id[] = { { MAX77976_DRIVER_NAME }, { } }; MODULE_DEVICE_TABLE(i2c, max77976_i2c_id); static const struct of_device_id max77976_of_id[] = { { .compatible = "maxim,max77976" }, { }, }; MODULE_DEVICE_TABLE(of, max77976_of_id); static struct i2c_driver max77976_driver = { .driver = { .name = MAX77976_DRIVER_NAME, .of_match_table = max77976_of_id, }, .probe = max77976_probe, .id_table = max77976_i2c_id, }; module_i2c_driver(max77976_driver); MODULE_AUTHOR("Luca Ceresoli "); MODULE_DESCRIPTION("Maxim MAX77976 charger driver"); MODULE_LICENSE("GPL v2");