510 lines
13 KiB
C
510 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* max77976_charger.c - Driver for the Maxim MAX77976 battery charger
|
|
*
|
|
* Copyright (C) 2021 Luca Ceresoli
|
|
* Author: Luca Ceresoli <luca@lucaceresoli.net>
|
|
*/
|
|
|
|
#include <linux/i2c.h>
|
|
#include <linux/module.h>
|
|
#include <linux/power_supply.h>
|
|
#include <linux/regmap.h>
|
|
|
|
#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;
|
|
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_no_ws(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, 0 },
|
|
{ },
|
|
};
|
|
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_new = max77976_probe,
|
|
.id_table = max77976_i2c_id,
|
|
};
|
|
module_i2c_driver(max77976_driver);
|
|
|
|
MODULE_AUTHOR("Luca Ceresoli <luca@lucaceresoli.net>");
|
|
MODULE_DESCRIPTION("Maxim MAX77976 charger driver");
|
|
MODULE_LICENSE("GPL v2");
|