root/drivers/power/supply/surface-rt-ec.c
// 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");