| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * power_supply class (battery) driver for the I2C attached embedded controller |
| * found on Vexia EDU ATLA 10 (9V version) tablets. |
| * |
| * This is based on the ACPI Battery device in the DSDT which should work |
| * expect that it expects the I2C controller to be enumerated as an ACPI |
| * device and the tablet's BIOS enumerates all LPSS devices as PCI devices |
| * (and changing the LPSS BIOS settings from PCI -> ACPI does not work). |
| * |
| * Copyright (c) 2024 Hans de Goede <hansg@kernel.org> |
| */ |
| |
| #include <linux/bits.h> |
| #include <linux/devm-helpers.h> |
| #include <linux/err.h> |
| #include <linux/i2c.h> |
| #include <linux/module.h> |
| #include <linux/power_supply.h> |
| #include <linux/types.h> |
| #include <linux/workqueue.h> |
| |
| #include <asm/byteorder.h> |
| |
| /* State field uses ACPI Battery spec status bits */ |
| #define ACPI_BATTERY_STATE_DISCHARGING BIT(0) |
| #define ACPI_BATTERY_STATE_CHARGING BIT(1) |
| |
| #define ATLA10_EC_BATTERY_STATE_COMMAND 0x87 |
| #define ATLA10_EC_BATTERY_INFO_COMMAND 0x88 |
| |
| /* From broken ACPI battery device in DSDT */ |
| #define ATLA10_EC_VOLTAGE_MIN_DESIGN_uV 3750000 |
| |
| /* Update data every 5 seconds */ |
| #define UPDATE_INTERVAL_JIFFIES (5 * HZ) |
| |
| struct atla10_ec_battery_state { |
| u8 status; /* Using ACPI Battery spec status bits */ |
| u8 capacity; /* Percent */ |
| __le16 charge_now_mAh; |
| __le16 voltage_now_mV; |
| __le16 current_now_mA; |
| __le16 charge_full_mAh; |
| __le16 temp; /* centi degrees Celsius */ |
| } __packed; |
| |
| struct atla10_ec_battery_info { |
| __le16 charge_full_design_mAh; |
| __le16 voltage_now_mV; /* Should be design voltage, but is not ? */ |
| __le16 charge_full_design2_mAh; |
| } __packed; |
| |
| struct atla10_ec_data { |
| struct i2c_client *client; |
| struct power_supply *psy; |
| struct delayed_work work; |
| struct mutex update_lock; |
| struct atla10_ec_battery_info info; |
| struct atla10_ec_battery_state state; |
| bool valid; /* true if state is valid */ |
| unsigned long last_update; /* In jiffies */ |
| }; |
| |
| static int atla10_ec_cmd(struct atla10_ec_data *data, u8 cmd, u8 len, u8 *values) |
| { |
| struct device *dev = &data->client->dev; |
| u8 buf[I2C_SMBUS_BLOCK_MAX]; |
| int ret; |
| |
| ret = i2c_smbus_read_block_data(data->client, cmd, buf); |
| if (ret != len) { |
| dev_err(dev, "I2C command 0x%02x error: %d\n", cmd, ret); |
| return -EIO; |
| } |
| |
| memcpy(values, buf, len); |
| return 0; |
| } |
| |
| static int atla10_ec_update(struct atla10_ec_data *data) |
| { |
| int ret; |
| |
| if (data->valid && time_before(jiffies, data->last_update + UPDATE_INTERVAL_JIFFIES)) |
| return 0; |
| |
| ret = atla10_ec_cmd(data, ATLA10_EC_BATTERY_STATE_COMMAND, |
| sizeof(data->state), (u8 *)&data->state); |
| if (ret) |
| return ret; |
| |
| data->last_update = jiffies; |
| data->valid = true; |
| return 0; |
| } |
| |
| static int atla10_ec_psy_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct atla10_ec_data *data = power_supply_get_drvdata(psy); |
| int charge_now_mAh, charge_full_mAh, ret; |
| |
| guard(mutex)(&data->update_lock); |
| |
| ret = atla10_ec_update(data); |
| if (ret) |
| return ret; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_STATUS: |
| if (data->state.status & ACPI_BATTERY_STATE_DISCHARGING) |
| val->intval = POWER_SUPPLY_STATUS_DISCHARGING; |
| else if (data->state.status & ACPI_BATTERY_STATE_CHARGING) |
| val->intval = POWER_SUPPLY_STATUS_CHARGING; |
| else if (data->state.capacity == 100) |
| val->intval = POWER_SUPPLY_STATUS_FULL; |
| else |
| val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; |
| break; |
| case POWER_SUPPLY_PROP_CAPACITY: |
| val->intval = data->state.capacity; |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_NOW: |
| /* |
| * The EC has a bug where it reports charge-full-design as |
| * charge-now when the battery is full. Clamp charge-now to |
| * charge-full to workaround this. |
| */ |
| charge_now_mAh = le16_to_cpu(data->state.charge_now_mAh); |
| charge_full_mAh = le16_to_cpu(data->state.charge_full_mAh); |
| val->intval = min(charge_now_mAh, charge_full_mAh) * 1000; |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| val->intval = le16_to_cpu(data->state.voltage_now_mV) * 1000; |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| val->intval = le16_to_cpu(data->state.current_now_mA) * 1000; |
| /* |
| * Documentation/ABI/testing/sysfs-class-power specifies |
| * negative current for discharging. |
| */ |
| if (data->state.status & ACPI_BATTERY_STATE_DISCHARGING) |
| val->intval = -val->intval; |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_FULL: |
| val->intval = le16_to_cpu(data->state.charge_full_mAh) * 1000; |
| break; |
| case POWER_SUPPLY_PROP_TEMP: |
| val->intval = le16_to_cpu(data->state.temp) / 10; |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: |
| val->intval = le16_to_cpu(data->info.charge_full_design_mAh) * 1000; |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: |
| val->intval = ATLA10_EC_VOLTAGE_MIN_DESIGN_uV; |
| break; |
| case POWER_SUPPLY_PROP_PRESENT: |
| val->intval = 1; |
| break; |
| case POWER_SUPPLY_PROP_TECHNOLOGY: |
| val->intval = POWER_SUPPLY_TECHNOLOGY_LIPO; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static void atla10_ec_external_power_changed_work(struct work_struct *work) |
| { |
| struct atla10_ec_data *data = container_of(work, struct atla10_ec_data, work.work); |
| |
| dev_dbg(&data->client->dev, "External power changed\n"); |
| data->valid = false; |
| power_supply_changed(data->psy); |
| } |
| |
| static void atla10_ec_external_power_changed(struct power_supply *psy) |
| { |
| struct atla10_ec_data *data = power_supply_get_drvdata(psy); |
| |
| /* After charger plug in/out wait 0.5s for things to stabilize */ |
| mod_delayed_work(system_wq, &data->work, HZ / 2); |
| } |
| |
| static const enum power_supply_property atla10_ec_psy_props[] = { |
| POWER_SUPPLY_PROP_STATUS, |
| POWER_SUPPLY_PROP_CAPACITY, |
| POWER_SUPPLY_PROP_CHARGE_NOW, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| POWER_SUPPLY_PROP_CURRENT_NOW, |
| POWER_SUPPLY_PROP_CHARGE_FULL, |
| POWER_SUPPLY_PROP_TEMP, |
| POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, |
| POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_TECHNOLOGY, |
| }; |
| |
| static const struct power_supply_desc atla10_ec_psy_desc = { |
| .name = "atla10_ec_battery", |
| .type = POWER_SUPPLY_TYPE_BATTERY, |
| .properties = atla10_ec_psy_props, |
| .num_properties = ARRAY_SIZE(atla10_ec_psy_props), |
| .get_property = atla10_ec_psy_get_property, |
| .external_power_changed = atla10_ec_external_power_changed, |
| }; |
| |
| static int atla10_ec_probe(struct i2c_client *client) |
| { |
| struct power_supply_config psy_cfg = { }; |
| struct device *dev = &client->dev; |
| struct atla10_ec_data *data; |
| int ret; |
| |
| data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| psy_cfg.drv_data = data; |
| data->client = client; |
| |
| ret = devm_mutex_init(dev, &data->update_lock); |
| if (ret) |
| return ret; |
| |
| ret = devm_delayed_work_autocancel(dev, &data->work, |
| atla10_ec_external_power_changed_work); |
| if (ret) |
| return ret; |
| |
| ret = atla10_ec_cmd(data, ATLA10_EC_BATTERY_INFO_COMMAND, |
| sizeof(data->info), (u8 *)&data->info); |
| if (ret) |
| return ret; |
| |
| data->psy = devm_power_supply_register(dev, &atla10_ec_psy_desc, &psy_cfg); |
| return PTR_ERR_OR_ZERO(data->psy); |
| } |
| |
| static const struct i2c_device_id atla10_ec_id_table[] = { |
| { "vexia_atla10_ec" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(i2c, atla10_ec_id_table); |
| |
| static struct i2c_driver atla10_ec_driver = { |
| .driver = { |
| .name = "vexia_atla10_ec", |
| }, |
| .probe = atla10_ec_probe, |
| .id_table = atla10_ec_id_table, |
| }; |
| module_i2c_driver(atla10_ec_driver); |
| |
| MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); |
| MODULE_DESCRIPTION("Battery driver for Vexia EDU ATLA 10 tablet EC"); |
| MODULE_LICENSE("GPL"); |