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