| // 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 ret; | |
| 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"); |