// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2022-2024, Linaro Ltd * Authors: * Bjorn Andersson * Dmitry Baryshkov */ #include #include #include #include #include #include #include #include #include #include struct yoga_c630_psy { struct yoga_c630_ec *ec; struct device *dev; struct fwnode_handle *fwnode; struct notifier_block nb; /* guards all battery properties and registration of power supplies */ struct mutex lock; struct power_supply *adp_psy; struct power_supply *bat_psy; unsigned long last_status_update; bool adapter_online; bool unit_mA; bool bat_present; unsigned int bat_status; unsigned int design_capacity; unsigned int design_voltage; unsigned int full_charge_capacity; unsigned int capacity_now; unsigned int voltage_now; int current_now; int rate_now; }; #define LENOVO_EC_CACHE_TIME (10 * HZ) #define LENOVO_EC_ADPT_STATUS 0xa3 #define LENOVO_EC_ADPT_STATUS_PRESENT BIT(7) #define LENOVO_EC_BAT_ATTRIBUTES 0xc0 #define LENOVO_EC_BAT_ATTRIBUTES_UNIT_IS_MA BIT(1) #define LENOVO_EC_BAT_STATUS 0xc1 #define LENOVO_EC_BAT_STATUS_DISCHARGING BIT(0) #define LENOVO_EC_BAT_STATUS_CHARGING BIT(1) #define LENOVO_EC_BAT_REMAIN_CAPACITY 0xc2 #define LENOVO_EC_BAT_VOLTAGE 0xc6 #define LENOVO_EC_BAT_DESIGN_VOLTAGE 0xc8 #define LENOVO_EC_BAT_DESIGN_CAPACITY 0xca #define LENOVO_EC_BAT_FULL_CAPACITY 0xcc #define LENOVO_EC_BAT_CURRENT 0xd2 #define LENOVO_EC_BAT_FULL_FACTORY 0xd6 #define LENOVO_EC_BAT_PRESENT 0xda #define LENOVO_EC_BAT_PRESENT_IS_PRESENT BIT(0) #define LENOVO_EC_BAT_FULL_REGISTER 0xdb #define LENOVO_EC_BAT_FULL_REGISTER_IS_FACTORY BIT(0) static int yoga_c630_psy_update_bat_info(struct yoga_c630_psy *ecbat) { struct yoga_c630_ec *ec = ecbat->ec; int val; lockdep_assert_held(&ecbat->lock); val = yoga_c630_ec_read8(ec, LENOVO_EC_BAT_PRESENT); if (val < 0) return val; ecbat->bat_present = !!(val & LENOVO_EC_BAT_PRESENT_IS_PRESENT); if (!ecbat->bat_present) return val; val = yoga_c630_ec_read8(ec, LENOVO_EC_BAT_ATTRIBUTES); if (val < 0) return val; ecbat->unit_mA = val & LENOVO_EC_BAT_ATTRIBUTES_UNIT_IS_MA; val = yoga_c630_ec_read16(ec, LENOVO_EC_BAT_DESIGN_CAPACITY); if (val < 0) return val; ecbat->design_capacity = val * 1000; /* * DSDT has delays after most of EC reads in these methods. * Having no documentation for the EC we have to follow and sleep here. */ msleep(50); val = yoga_c630_ec_read16(ec, LENOVO_EC_BAT_DESIGN_VOLTAGE); if (val < 0) return val; ecbat->design_voltage = val; msleep(50); val = yoga_c630_ec_read8(ec, LENOVO_EC_BAT_FULL_REGISTER); if (val < 0) return val; val = yoga_c630_ec_read16(ec, val & LENOVO_EC_BAT_FULL_REGISTER_IS_FACTORY ? LENOVO_EC_BAT_FULL_FACTORY : LENOVO_EC_BAT_FULL_CAPACITY); if (val < 0) return val; ecbat->full_charge_capacity = val * 1000; if (!ecbat->unit_mA) { ecbat->design_capacity *= 10; ecbat->full_charge_capacity *= 10; } return 0; } static int yoga_c630_psy_maybe_update_bat_status(struct yoga_c630_psy *ecbat) { struct yoga_c630_ec *ec = ecbat->ec; int current_mA; int val; guard(mutex)(&ecbat->lock); if (time_before(jiffies, ecbat->last_status_update + LENOVO_EC_CACHE_TIME)) return 0; val = yoga_c630_ec_read8(ec, LENOVO_EC_BAT_STATUS); if (val < 0) return val; ecbat->bat_status = val; msleep(50); val = yoga_c630_ec_read16(ec, LENOVO_EC_BAT_REMAIN_CAPACITY); if (val < 0) return val; ecbat->capacity_now = val * 1000; msleep(50); val = yoga_c630_ec_read16(ec, LENOVO_EC_BAT_VOLTAGE); if (val < 0) return val; ecbat->voltage_now = val * 1000; msleep(50); val = yoga_c630_ec_read16(ec, LENOVO_EC_BAT_CURRENT); if (val < 0) return val; current_mA = sign_extend32(val, 15); ecbat->current_now = current_mA * 1000; ecbat->rate_now = current_mA * (ecbat->voltage_now / 1000); msleep(50); if (!ecbat->unit_mA) ecbat->capacity_now *= 10; ecbat->last_status_update = jiffies; return 0; } static int yoga_c630_psy_update_adapter_status(struct yoga_c630_psy *ecbat) { struct yoga_c630_ec *ec = ecbat->ec; int val; guard(mutex)(&ecbat->lock); val = yoga_c630_ec_read8(ec, LENOVO_EC_ADPT_STATUS); if (val < 0) return val; ecbat->adapter_online = !!(val & LENOVO_EC_ADPT_STATUS_PRESENT); return 0; } static bool yoga_c630_psy_is_charged(struct yoga_c630_psy *ecbat) { if (ecbat->bat_status != 0) return false; if (ecbat->full_charge_capacity <= ecbat->capacity_now) return true; if (ecbat->design_capacity <= ecbat->capacity_now) return true; return false; } static int yoga_c630_psy_bat_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct yoga_c630_psy *ecbat = power_supply_get_drvdata(psy); int rc = 0; if (!ecbat->bat_present && psp != POWER_SUPPLY_PROP_PRESENT) return -ENODEV; rc = yoga_c630_psy_maybe_update_bat_status(ecbat); if (rc) return rc; switch (psp) { case POWER_SUPPLY_PROP_STATUS: if (ecbat->bat_status & LENOVO_EC_BAT_STATUS_DISCHARGING) val->intval = POWER_SUPPLY_STATUS_DISCHARGING; else if (ecbat->bat_status & LENOVO_EC_BAT_STATUS_CHARGING) val->intval = POWER_SUPPLY_STATUS_CHARGING; else if (yoga_c630_psy_is_charged(ecbat)) val->intval = POWER_SUPPLY_STATUS_FULL; else val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; break; case POWER_SUPPLY_PROP_PRESENT: val->intval = ecbat->bat_present; break; case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: val->intval = ecbat->design_voltage; break; case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: val->intval = ecbat->design_capacity; break; case POWER_SUPPLY_PROP_CHARGE_FULL: case POWER_SUPPLY_PROP_ENERGY_FULL: val->intval = ecbat->full_charge_capacity; break; case POWER_SUPPLY_PROP_CHARGE_NOW: case POWER_SUPPLY_PROP_ENERGY_NOW: val->intval = ecbat->capacity_now; break; case POWER_SUPPLY_PROP_CURRENT_NOW: val->intval = ecbat->current_now; break; case POWER_SUPPLY_PROP_POWER_NOW: val->intval = ecbat->rate_now; break; case POWER_SUPPLY_PROP_VOLTAGE_NOW: val->intval = ecbat->voltage_now; break; case POWER_SUPPLY_PROP_TECHNOLOGY: val->intval = POWER_SUPPLY_TECHNOLOGY_LION; break; case POWER_SUPPLY_PROP_MODEL_NAME: val->strval = "PABAS0241231"; break; case POWER_SUPPLY_PROP_MANUFACTURER: val->strval = "Compal"; break; case POWER_SUPPLY_PROP_SCOPE: val->intval = POWER_SUPPLY_SCOPE_SYSTEM; break; default: rc = -EINVAL; break; } return rc; } static enum power_supply_property yoga_c630_psy_bat_mA_properties[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT, POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, POWER_SUPPLY_PROP_CHARGE_FULL, POWER_SUPPLY_PROP_CHARGE_NOW, POWER_SUPPLY_PROP_CURRENT_NOW, POWER_SUPPLY_PROP_POWER_NOW, POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_TECHNOLOGY, POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER, POWER_SUPPLY_PROP_SCOPE, }; static enum power_supply_property yoga_c630_psy_bat_mWh_properties[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT, POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, POWER_SUPPLY_PROP_ENERGY_FULL, POWER_SUPPLY_PROP_ENERGY_NOW, POWER_SUPPLY_PROP_CURRENT_NOW, POWER_SUPPLY_PROP_POWER_NOW, POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_TECHNOLOGY, POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER, POWER_SUPPLY_PROP_SCOPE, }; static const struct power_supply_desc yoga_c630_psy_bat_psy_desc_mA = { .name = "yoga-c630-battery", .type = POWER_SUPPLY_TYPE_BATTERY, .properties = yoga_c630_psy_bat_mA_properties, .num_properties = ARRAY_SIZE(yoga_c630_psy_bat_mA_properties), .get_property = yoga_c630_psy_bat_get_property, }; static const struct power_supply_desc yoga_c630_psy_bat_psy_desc_mWh = { .name = "yoga-c630-battery", .type = POWER_SUPPLY_TYPE_BATTERY, .properties = yoga_c630_psy_bat_mWh_properties, .num_properties = ARRAY_SIZE(yoga_c630_psy_bat_mWh_properties), .get_property = yoga_c630_psy_bat_get_property, }; static int yoga_c630_psy_adpt_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct yoga_c630_psy *ecbat = power_supply_get_drvdata(psy); int ret = 0; ret = yoga_c630_psy_update_adapter_status(ecbat); if (ret < 0) return ret; switch (psp) { case POWER_SUPPLY_PROP_ONLINE: val->intval = ecbat->adapter_online; break; case POWER_SUPPLY_PROP_USB_TYPE: val->intval = POWER_SUPPLY_USB_TYPE_C; break; default: return -EINVAL; } return 0; } static enum power_supply_property yoga_c630_psy_adpt_properties[] = { POWER_SUPPLY_PROP_ONLINE, POWER_SUPPLY_PROP_USB_TYPE, }; static const enum power_supply_usb_type yoga_c630_psy_adpt_usb_type[] = { POWER_SUPPLY_USB_TYPE_C, }; static const struct power_supply_desc yoga_c630_psy_adpt_psy_desc = { .name = "yoga-c630-adapter", .type = POWER_SUPPLY_TYPE_USB, .usb_types = yoga_c630_psy_adpt_usb_type, .num_usb_types = ARRAY_SIZE(yoga_c630_psy_adpt_usb_type), .properties = yoga_c630_psy_adpt_properties, .num_properties = ARRAY_SIZE(yoga_c630_psy_adpt_properties), .get_property = yoga_c630_psy_adpt_get_property, }; static int yoga_c630_psy_register_bat_psy(struct yoga_c630_psy *ecbat) { struct power_supply_config bat_cfg = {}; bat_cfg.drv_data = ecbat; bat_cfg.fwnode = ecbat->fwnode; ecbat->bat_psy = power_supply_register_no_ws(ecbat->dev, ecbat->unit_mA ? &yoga_c630_psy_bat_psy_desc_mA : &yoga_c630_psy_bat_psy_desc_mWh, &bat_cfg); if (IS_ERR(ecbat->bat_psy)) { dev_err(ecbat->dev, "failed to register battery supply\n"); return PTR_ERR(ecbat->bat_psy); } return 0; } static void yoga_c630_ec_refresh_bat_info(struct yoga_c630_psy *ecbat) { bool current_unit; guard(mutex)(&ecbat->lock); current_unit = ecbat->unit_mA; yoga_c630_psy_update_bat_info(ecbat); if (current_unit != ecbat->unit_mA) { power_supply_unregister(ecbat->bat_psy); yoga_c630_psy_register_bat_psy(ecbat); } } static int yoga_c630_psy_notify(struct notifier_block *nb, unsigned long action, void *data) { struct yoga_c630_psy *ecbat = container_of(nb, struct yoga_c630_psy, nb); switch (action) { case LENOVO_EC_EVENT_BAT_INFO: yoga_c630_ec_refresh_bat_info(ecbat); break; case LENOVO_EC_EVENT_BAT_ADPT_STATUS: power_supply_changed(ecbat->adp_psy); fallthrough; case LENOVO_EC_EVENT_BAT_STATUS: power_supply_changed(ecbat->bat_psy); break; } return NOTIFY_OK; } static int yoga_c630_psy_probe(struct auxiliary_device *adev, const struct auxiliary_device_id *id) { struct yoga_c630_ec *ec = adev->dev.platform_data; struct power_supply_config adp_cfg = {}; struct device *dev = &adev->dev; struct yoga_c630_psy *ecbat; int ret; ecbat = devm_kzalloc(&adev->dev, sizeof(*ecbat), GFP_KERNEL); if (!ecbat) return -ENOMEM; ecbat->ec = ec; ecbat->dev = dev; mutex_init(&ecbat->lock); ecbat->fwnode = adev->dev.parent->fwnode; ecbat->nb.notifier_call = yoga_c630_psy_notify; auxiliary_set_drvdata(adev, ecbat); adp_cfg.drv_data = ecbat; adp_cfg.fwnode = ecbat->fwnode; adp_cfg.supplied_to = (char **)&yoga_c630_psy_bat_psy_desc_mA.name; adp_cfg.num_supplicants = 1; ecbat->adp_psy = devm_power_supply_register_no_ws(dev, &yoga_c630_psy_adpt_psy_desc, &adp_cfg); if (IS_ERR(ecbat->adp_psy)) { dev_err(dev, "failed to register AC adapter supply\n"); return PTR_ERR(ecbat->adp_psy); } scoped_guard(mutex, &ecbat->lock) { ret = yoga_c630_psy_update_bat_info(ecbat); if (ret) goto err_unreg_bat; ret = yoga_c630_psy_register_bat_psy(ecbat); if (ret) goto err_unreg_bat; } ret = yoga_c630_ec_register_notify(ecbat->ec, &ecbat->nb); if (ret) goto err_unreg_bat; return 0; err_unreg_bat: power_supply_unregister(ecbat->bat_psy); return ret; } static void yoga_c630_psy_remove(struct auxiliary_device *adev) { struct yoga_c630_psy *ecbat = auxiliary_get_drvdata(adev); yoga_c630_ec_unregister_notify(ecbat->ec, &ecbat->nb); power_supply_unregister(ecbat->bat_psy); } static const struct auxiliary_device_id yoga_c630_psy_id_table[] = { { .name = YOGA_C630_MOD_NAME "." YOGA_C630_DEV_PSY, }, {} }; MODULE_DEVICE_TABLE(auxiliary, yoga_c630_psy_id_table); static struct auxiliary_driver yoga_c630_psy_driver = { .name = YOGA_C630_DEV_PSY, .id_table = yoga_c630_psy_id_table, .probe = yoga_c630_psy_probe, .remove = yoga_c630_psy_remove, }; module_auxiliary_driver(yoga_c630_psy_driver); MODULE_DESCRIPTION("Lenovo Yoga C630 psy"); MODULE_LICENSE("GPL");