diff options
Diffstat (limited to 'drivers/power/supply/surface-rt-ec.c')
| -rw-r--r-- | drivers/power/supply/surface-rt-ec.c | 389 |
1 files changed, 389 insertions, 0 deletions
diff --git a/drivers/power/supply/surface-rt-ec.c b/drivers/power/supply/surface-rt-ec.c new file mode 100644 index 000000000..a728ae0db --- /dev/null +++ b/drivers/power/supply/surface-rt-ec.c @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include <linux/devm-helpers.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/power_supply.h> +#include <linux/types.h> + +/* Register Addresses (B=byte; W=word; S=string) */ +#define REGB_STATUS 0x02 +#define REGW_VOLTAGE_NOW 0x20 +#define REGW_CURRENT_NOW 0x24 +#define REGW_CAPACITY 0x28 +#define REGW_CHARGE_NOW 0x2a +#define REGW_CHARGE_FULL 0x2c +#define REGW_CYCLE_COUNT 0x3a +#define REGW_CHARGE_FULL_DESIGN 0x3c +#define REGW_VOLTAGE_MAX_DESIGN 0x3e +#define REGW_SERIAL_NUMBER 0x44 +#define REGS_MANUFACTURER 0x46 +#define REGS_MODEL_NAME 0x52 +#define REGS_TECHNOLOGY 0x5a +#define REGB_ONLINE 0x67 + +struct srt_ec_device { + struct i2c_client *client; + + struct power_supply *bat; + struct power_supply *psy; + + struct gpio_desc *enable_gpiod; + struct delayed_work poll_work; + + unsigned int technology; + unsigned int capacity; + + const char *serial; + char manufacturer[13]; + char model_name[10]; +}; + +static const enum power_supply_property srt_bat_power_supply_props[] = { + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, +}; + +static const enum power_supply_property srt_psy_power_supply_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_PRESENT, +}; + +static int srt_bat_get_value(struct i2c_client *client, int reg, int *val) +{ + int ret; + + switch (reg) { + case REGW_CHARGE_NOW: + case REGW_CHARGE_FULL_DESIGN: + case REGW_CHARGE_FULL: + case REGW_VOLTAGE_MAX_DESIGN: + case REGW_VOLTAGE_NOW: + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + *val = ret * 1000; + break; + + case REGW_CURRENT_NOW: + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + *val = (s16)ret * 1000; + break; + + case REGW_CAPACITY: + case REGW_CYCLE_COUNT: + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + *val = ret; + break; + + case REGB_STATUS: + ret = i2c_smbus_read_byte_data(client, reg); + if (ret < 0) + return ret; + + if (ret & BIT(0)) + *val = POWER_SUPPLY_STATUS_CHARGING; + else + *val = POWER_SUPPLY_STATUS_DISCHARGING; + break; + + case REGB_ONLINE: + ret = i2c_smbus_read_byte_data(client, reg); + if (ret < 0) + return ret; + + *val = (ret & BIT(1)) >> 1; + break; + + default: + return -EINVAL; + } + + return 0; +} + +static int srt_bat_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct srt_ec_device *srt = power_supply_get_drvdata(psy); + struct i2c_client *client = srt->client; + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = srt->manufacturer; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = srt->model_name; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = srt->serial; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = srt_bat_get_value(client, REGW_CAPACITY, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = srt_bat_get_value(client, REGW_CHARGE_NOW, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = srt_bat_get_value(client, REGW_CHARGE_FULL, &val->intval); + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = srt_bat_get_value(client, REGW_CHARGE_FULL_DESIGN, + &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = srt_bat_get_value(client, REGW_CURRENT_NOW, &val->intval); + break; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + ret = srt_bat_get_value(client, REGW_CYCLE_COUNT, &val->intval); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_ONLINE: + ret = srt_bat_get_value(client, REGB_ONLINE, &val->intval); + break; + case POWER_SUPPLY_PROP_STATUS: + if (srt->capacity < 100) + ret = srt_bat_get_value(client, REGB_STATUS, &val->intval); + else + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = srt->technology; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + ret = srt_bat_get_value(client, REGW_VOLTAGE_MAX_DESIGN, + &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = srt_bat_get_value(client, REGW_VOLTAGE_NOW, &val->intval); + break; + default: + return -EINVAL; + } + + return ret; +} + +static int srt_psy_power_supply_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct srt_ec_device *srt = power_supply_get_drvdata(psy); + struct i2c_client *client = srt->client; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + case POWER_SUPPLY_PROP_PRESENT: + ret = i2c_smbus_read_byte_data(client, REGB_ONLINE); + if (ret < 0) + return ret; + + val->intval = ret & BIT(0); + break; + default: + return -EINVAL; + } + + return 0; +} + +static void srt_bat_poll_work(struct work_struct *work) +{ + struct srt_ec_device *srt = + container_of(work, struct srt_ec_device, poll_work.work); + int ret, capacity; + + ret = srt_bat_get_value(srt->client, REGW_CAPACITY, &capacity); + if (!ret && capacity != srt->capacity) { + srt->capacity = capacity; + power_supply_changed(srt->bat); + } + + /* continuously send uevent notification */ + schedule_delayed_work(&srt->poll_work, 30 * HZ); +} + +static irqreturn_t srt_psy_detect_irq(int irq, void *dev_id) +{ + struct srt_ec_device *srt = dev_id; + + power_supply_changed(srt->psy); + + return IRQ_HANDLED; +} + +static const struct power_supply_desc srt_bat_power_supply_desc = { + .name = "surface-rt-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = srt_bat_power_supply_props, + .num_properties = ARRAY_SIZE(srt_bat_power_supply_props), + .get_property = srt_bat_power_supply_get_property, + .external_power_changed = power_supply_changed, +}; + +static const struct power_supply_desc srt_psy_power_supply_desc = { + .name = "surface-rt-ac-adapter", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = srt_psy_power_supply_props, + .num_properties = ARRAY_SIZE(srt_psy_power_supply_props), + .get_property = srt_psy_power_supply_get_property, +}; + +static char *battery_supplied_to[] = { "surface-rt-battery" }; + +static int srt_ec_probe(struct i2c_client *client) +{ + struct power_supply_config bat_cfg = {}; + struct power_supply_config psy_cfg = {}; + struct device *dev = &client->dev; + struct srt_ec_device *srt; + char str_buf[4]; + int ret; + + srt = devm_kzalloc(dev, sizeof(*srt), GFP_KERNEL); + if (!srt) + return -ENOMEM; + + i2c_set_clientdata(client, srt); + srt->client = client; + + srt->enable_gpiod = devm_gpiod_get(dev, "enable", GPIOD_OUT_HIGH); + if (IS_ERR(srt->enable_gpiod)) + return dev_err_probe(dev, PTR_ERR(srt->enable_gpiod), + "failed to get enable gpio\n"); + + /* wait till EC is ready */ + usleep_range(1000, 1500); + + ret = i2c_smbus_read_word_data(client, REGW_SERIAL_NUMBER); + if (ret < 0) + return ret; + + srt->serial = devm_kasprintf(dev, GFP_KERNEL, "%04x", ret); + if (!srt->serial) + return -ENOMEM; + + ret = i2c_smbus_read_i2c_block_data(client, REGS_MANUFACTURER, + sizeof(srt->manufacturer) - 1, + srt->manufacturer); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_i2c_block_data(client, REGS_MODEL_NAME, + sizeof(srt->model_name) - 1, + srt->model_name); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_i2c_block_data(client, REGS_TECHNOLOGY, + sizeof(str_buf), str_buf); + if (ret < 0) + return ret; + + if (!strncmp(str_buf, "LION", 4)) + srt->technology = POWER_SUPPLY_TECHNOLOGY_LION; + else + srt->technology = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; + + bat_cfg.drv_data = srt; + bat_cfg.fwnode = dev_fwnode(dev); + + srt->bat = devm_power_supply_register(dev, &srt_bat_power_supply_desc, + &bat_cfg); + if (IS_ERR(srt->bat)) + return dev_err_probe(dev, PTR_ERR(srt->bat), + "failed to register battery power supply\n"); + + psy_cfg.drv_data = srt; + psy_cfg.fwnode = dev_fwnode(dev); + psy_cfg.supplied_to = battery_supplied_to; + psy_cfg.num_supplicants = ARRAY_SIZE(battery_supplied_to); + + srt->psy = devm_power_supply_register(dev, &srt_psy_power_supply_desc, + &psy_cfg); + if (IS_ERR(srt->psy)) + return dev_err_probe(dev, PTR_ERR(srt->psy), + "failed to register AC power supply\n"); + + ret = devm_request_threaded_irq(dev, client->irq, NULL, srt_psy_detect_irq, + IRQF_ONESHOT, client->name, srt); + if (ret < 0) + return dev_err_probe(dev, ret, "failed to request interrupt\n"); + + ret = devm_delayed_work_autocancel(dev, &srt->poll_work, srt_bat_poll_work); + if (ret < 0) + return ret; + + schedule_delayed_work(&srt->poll_work, HZ); + + return 0; +} + +static int srt_ec_suspend(struct device *dev) +{ + struct srt_ec_device *srt = dev_get_drvdata(dev); + + cancel_delayed_work_sync(&srt->poll_work); + + return 0; +} + +static int srt_ec_resume(struct device *dev) +{ + struct srt_ec_device *srt = dev_get_drvdata(dev); + + schedule_delayed_work(&srt->poll_work, HZ); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(srt_ec_pm_ops, srt_ec_suspend, srt_ec_resume); + +static const struct of_device_id srt_ec_of_match[] = { + { .compatible = "microsoft,surface-rt-ec" }, + { } +}; +MODULE_DEVICE_TABLE(of, srt_ec_of_match); + +static struct i2c_driver srt_ec_driver = { + .driver = { + .name = "surface-rt-ec", + .of_match_table = srt_ec_of_match, + .pm = &srt_ec_pm_ops, + }, + .probe = srt_ec_probe, +}; +module_i2c_driver(srt_ec_driver); + +MODULE_AUTHOR("Jonas Schwöbel <jonasschwoebel@yahoo.de>"); +MODULE_DESCRIPTION("Surface RT Embedded Controller driver"); +MODULE_LICENSE("GPL"); |
