diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
commit | 2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch) | |
tree | 848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/power/supply/cpcap-battery.c | |
parent | Initial commit. (diff) | |
download | linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.tar.xz linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.zip |
Adding upstream version 6.1.76.upstream/6.1.76upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | drivers/power/supply/cpcap-battery.c | 1180 |
1 files changed, 1180 insertions, 0 deletions
diff --git a/drivers/power/supply/cpcap-battery.c b/drivers/power/supply/cpcap-battery.c new file mode 100644 index 000000000..d98d9244e --- /dev/null +++ b/drivers/power/supply/cpcap-battery.c @@ -0,0 +1,1180 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Battery driver for CPCAP PMIC + * + * Copyright (C) 2017 Tony Lindgren <tony@atomide.com> + * + * Some parts of the code based on earlier Motorola mapphone Linux kernel + * drivers: + * + * Copyright (C) 2009-2010 Motorola, Inc. + */ + +#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/nvmem-consumer.h> +#include <linux/moduleparam.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 + +#define CPCAP_BATTERY_EB41_HW4X_ID 0x9E +#define CPCAP_BATTERY_BW8X_ID 0x98 + +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_EMPTY, + CPCAP_BATTERY_STATE_FULL, + 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 charge_full; + int status; + u16 vendor; + bool check_nvmem; + unsigned int is_full:1; +}; + +#define CPCAP_NO_BATTERY -400 + +static bool ignore_temperature_probe; +module_param(ignore_temperature_probe, bool, 0660); + +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 struct cpcap_battery_state_data * +cpcap_battery_get_empty(struct cpcap_battery_ddata *ddata) +{ + return cpcap_battery_get_state(ddata, CPCAP_BATTERY_STATE_EMPTY); +} + +static struct cpcap_battery_state_data * +cpcap_battery_get_full(struct cpcap_battery_ddata *ddata) +{ + return cpcap_battery_get_state(ddata, CPCAP_BATTERY_STATE_FULL); +} + +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) { + if (!ignore_temperature_probe) + 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); +} + + +/* + * Based on the values from Motorola mapphone Linux kernel for the + * stock Droid 4 battery eb41. In 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. These values are also fine for + * Bionic's hw4x. + * + * 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_eb41_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, +}; + +/* Values for the extended Droid Bionic battery bw8x. */ +static const struct cpcap_battery_config cpcap_battery_bw8x_data = { + .cd_factor = 0x3cc, + .info.technology = POWER_SUPPLY_TECHNOLOGY_LION, + .info.voltage_max_design = 4200000, + .info.voltage_min_design = 3200000, + .info.charge_full_design = 2760000, + .bat.constant_charge_voltage_max_uv = 4200000, +}; + +/* + * Safe values for any lipo battery likely to fit into a mapphone + * battery bay. + */ +static const struct cpcap_battery_config cpcap_battery_unkown_data = { + .cd_factor = 0x3cc, + .info.technology = POWER_SUPPLY_TECHNOLOGY_LION, + .info.voltage_max_design = 4200000, + .info.voltage_min_design = 3200000, + .info.charge_full_design = 3000000, + .bat.constant_charge_voltage_max_uv = 4200000, +}; + +static int cpcap_battery_match_nvmem(struct device *dev, const void *data) +{ + if (strcmp(dev_name(dev), "89-500029ba0f73") == 0) + return 1; + else + return 0; +} + +static void cpcap_battery_detect_battery_type(struct cpcap_battery_ddata *ddata) +{ + struct nvmem_device *nvmem; + u8 battery_id = 0; + + ddata->check_nvmem = false; + + nvmem = nvmem_device_find(NULL, &cpcap_battery_match_nvmem); + if (IS_ERR_OR_NULL(nvmem)) { + ddata->check_nvmem = true; + dev_info_once(ddata->dev, "Can not find battery nvmem device. Assuming generic lipo battery\n"); + } else if (nvmem_device_read(nvmem, 2, 1, &battery_id) < 0) { + battery_id = 0; + ddata->check_nvmem = true; + dev_warn(ddata->dev, "Can not read battery nvmem device. Assuming generic lipo battery\n"); + } + + switch (battery_id) { + case CPCAP_BATTERY_EB41_HW4X_ID: + ddata->config = cpcap_battery_eb41_data; + break; + case CPCAP_BATTERY_BW8X_ID: + ddata->config = cpcap_battery_bw8x_data; + break; + default: + ddata->config = cpcap_battery_unkown_data; + } +} + +/** + * 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 int cpcap_battery_get_charger_status(struct cpcap_battery_ddata *ddata, + int *val) +{ + union power_supply_propval prop; + 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_STATUS, + &prop); + if (error) + *val = POWER_SUPPLY_STATUS_UNKNOWN; + else + *val = prop.intval; + + power_supply_put(charger); + + return error; +} + +static bool cpcap_battery_full(struct cpcap_battery_ddata *ddata) +{ + struct cpcap_battery_state_data *state = cpcap_battery_latest(ddata); + unsigned int vfull; + int error, val; + + error = cpcap_battery_get_charger_status(ddata, &val); + if (!error) { + switch (val) { + case POWER_SUPPLY_STATUS_DISCHARGING: + dev_dbg(ddata->dev, "charger disconnected\n"); + ddata->is_full = 0; + break; + case POWER_SUPPLY_STATUS_FULL: + dev_dbg(ddata->dev, "charger full status\n"); + ddata->is_full = 1; + break; + default: + break; + } + } + + /* + * The full battery voltage here can be inaccurate, it's used just to + * filter out any trickle charging events. We clear the is_full status + * on charger disconnect above anyways. + */ + vfull = ddata->config.bat.constant_charge_voltage_max_uv - 120000; + + if (ddata->is_full && state->voltage < vfull) + ddata->is_full = 0; + + return ddata->is_full; +} + +static bool cpcap_battery_low(struct cpcap_battery_ddata *ddata) +{ + struct cpcap_battery_state_data *state = cpcap_battery_latest(ddata); + static bool is_low; + + if (state->current_ua > 0 && (state->voltage <= 3350000 || is_low)) + is_low = true; + else + is_low = false; + + return is_low; +} + +static int cpcap_battery_update_status(struct cpcap_battery_ddata *ddata) +{ + struct cpcap_battery_state_data state, *latest, *previous, + *empty, *full; + 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)); + + if (cpcap_battery_full(ddata)) { + full = cpcap_battery_get_full(ddata); + memcpy(full, latest, sizeof(*full)); + + empty = cpcap_battery_get_empty(ddata); + if (empty->voltage && empty->voltage != -1) { + empty->voltage = -1; + ddata->charge_full = + empty->counter_uah - full->counter_uah; + } else if (ddata->charge_full) { + empty->voltage = -1; + empty->counter_uah = + full->counter_uah + ddata->charge_full; + } + } else if (cpcap_battery_low(ddata)) { + empty = cpcap_battery_get_empty(ddata); + memcpy(empty, latest, sizeof(*empty)); + + full = cpcap_battery_get_full(ddata); + if (full->voltage) { + full->voltage = 0; + ddata->charge_full = + empty->counter_uah - full->counter_uah; + } + } + + return 0; +} + +/* + * Update battery status when cpcap-charger calls power_supply_changed(). + * This allows us to detect battery full condition before the charger + * disconnects. + */ +static void cpcap_battery_external_power_changed(struct power_supply *psy) +{ + union power_supply_propval prop; + + power_supply_get_property(psy, POWER_SUPPLY_PROP_STATUS, &prop); +} + +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, + POWER_SUPPLY_PROP_CHARGE_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, + 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, *empty; + 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); + + if (ddata->check_nvmem) + cpcap_battery_detect_battery_type(ddata); + + switch (psp) { + case POWER_SUPPLY_PROP_PRESENT: + if (latest->temperature > CPCAP_NO_BATTERY || ignore_temperature_probe) + 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: + empty = cpcap_battery_get_empty(ddata); + if (!empty->voltage || !ddata->charge_full) + return -ENODATA; + /* (ddata->charge_full / 200) is needed for rounding */ + val->intval = empty->counter_uah - latest->counter_uah + + ddata->charge_full / 200; + val->intval = clamp(val->intval, 0, ddata->charge_full); + val->intval = val->intval * 100 / ddata->charge_full; + 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_NOW: + empty = cpcap_battery_get_empty(ddata); + if (!empty->voltage) + return -ENODATA; + val->intval = empty->counter_uah - latest->counter_uah; + if (val->intval < 0) { + /* Assume invalid config if CHARGE_NOW is -20% */ + if (ddata->charge_full && abs(val->intval) > ddata->charge_full/5) { + empty->voltage = 0; + ddata->charge_full = 0; + return -ENODATA; + } + val->intval = 0; + } else if (ddata->charge_full && ddata->charge_full < val->intval) { + /* Assume invalid config if CHARGE_NOW exceeds CHARGE_FULL by 20% */ + if (val->intval > (6*ddata->charge_full)/5) { + empty->voltage = 0; + ddata->charge_full = 0; + return -ENODATA; + } + val->intval = ddata->charge_full; + } + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + if (!ddata->charge_full) + return -ENODATA; + val->intval = ddata->charge_full; + 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: + if (ignore_temperature_probe) + return -ENODATA; + 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); + case POWER_SUPPLY_PROP_CHARGE_FULL: + if (val->intval < 0) + return -EINVAL; + if (val->intval > (6*ddata->config.info.charge_full_design)/5) + return -EINVAL; + + ddata->charge_full = val->intval; + + return 0; + 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: + case POWER_SUPPLY_PROP_CHARGE_FULL: + 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; +} + +#ifdef CONFIG_OF +static const struct of_device_id cpcap_battery_id_table[] = { + { + .compatible = "motorola,cpcap-battery", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, cpcap_battery_id_table); +#endif + +static const struct power_supply_desc cpcap_charger_battery_desc = { + .name = "battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = cpcap_battery_props, + .num_properties = ARRAY_SIZE(cpcap_battery_props), + .get_property = cpcap_battery_get_property, + .set_property = cpcap_battery_set_property, + .property_is_writeable = cpcap_battery_property_is_writeable, + .external_power_changed = cpcap_battery_external_power_changed, +}; + +static int cpcap_battery_probe(struct platform_device *pdev) +{ + struct cpcap_battery_ddata *ddata; + struct power_supply_config psy_cfg = {}; + int error; + + ddata = devm_kzalloc(&pdev->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + cpcap_battery_detect_battery_type(ddata); + + INIT_LIST_HEAD(&ddata->irq_list); + ddata->dev = &pdev->dev; + + 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_cfg.of_node = pdev->dev.of_node; + psy_cfg.drv_data = ddata; + + ddata->psy = devm_power_supply_register(ddata->dev, + &cpcap_charger_battery_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"); |