// SPDX-License-Identifier: GPL-2.0 /* * nvec_power: power supply driver for a NVIDIA compliant embedded controller * * Copyright (C) 2011 The AC100 Kernel Team <ac100@lists.launchpad.net> * * Authors: Ilya Petrov <ilya.muromec@gmail.com> * Marc Dietrich <marvin24@gmx.de> */ #include <linux/module.h> #include <linux/platform_device.h> #include <linux/err.h> #include <linux/power_supply.h> #include <linux/slab.h> #include <linux/workqueue.h> #include <linux/delay.h> #include "nvec.h" #define GET_SYSTEM_STATUS 0x00 struct nvec_power { struct notifier_block notifier; struct delayed_work poller; struct nvec_chip *nvec; int on; int bat_present; int bat_status; int bat_voltage_now; int bat_current_now; int bat_current_avg; int time_remain; int charge_full_design; int charge_last_full; int critical_capacity; int capacity_remain; int bat_temperature; int bat_cap; int bat_type_enum; char bat_manu[30]; char bat_model[30]; char bat_type[30]; }; enum { SLOT_STATUS, VOLTAGE, TIME_REMAINING, CURRENT, AVERAGE_CURRENT, AVERAGING_TIME_INTERVAL, CAPACITY_REMAINING, LAST_FULL_CHARGE_CAPACITY, DESIGN_CAPACITY, CRITICAL_CAPACITY, TEMPERATURE, MANUFACTURER, MODEL, TYPE, }; enum { AC, BAT, }; struct bat_response { u8 event_type; u8 length; u8 sub_type; u8 status; /* payload */ union { char plc[30]; u16 plu; s16 pls; }; }; static struct power_supply *nvec_bat_psy; static struct power_supply *nvec_psy; static int nvec_power_notifier(struct notifier_block *nb, unsigned long event_type, void *data) { struct nvec_power *power = container_of(nb, struct nvec_power, notifier); struct bat_response *res = data; if (event_type != NVEC_SYS) return NOTIFY_DONE; if (res->sub_type == 0) { if (power->on != res->plu) { power->on = res->plu; power_supply_changed(nvec_psy); } return NOTIFY_STOP; } return NOTIFY_OK; } static const int bat_init[] = { LAST_FULL_CHARGE_CAPACITY, DESIGN_CAPACITY, CRITICAL_CAPACITY, MANUFACTURER, MODEL, TYPE, }; static void get_bat_mfg_data(struct nvec_power *power) { int i; char buf[] = { NVEC_BAT, SLOT_STATUS }; for (i = 0; i < ARRAY_SIZE(bat_init); i++) { buf[1] = bat_init[i]; nvec_write_async(power->nvec, buf, 2); } } static int nvec_power_bat_notifier(struct notifier_block *nb, unsigned long event_type, void *data) { struct nvec_power *power = container_of(nb, struct nvec_power, notifier); struct bat_response *res = data; int status_changed = 0; if (event_type != NVEC_BAT) return NOTIFY_DONE; switch (res->sub_type) { case SLOT_STATUS: if (res->plc[0] & 1) { if (power->bat_present == 0) { status_changed = 1; get_bat_mfg_data(power); } power->bat_present = 1; switch ((res->plc[0] >> 1) & 3) { case 0: power->bat_status = POWER_SUPPLY_STATUS_NOT_CHARGING; break; case 1: power->bat_status = POWER_SUPPLY_STATUS_CHARGING; break; case 2: power->bat_status = POWER_SUPPLY_STATUS_DISCHARGING; break; default: power->bat_status = POWER_SUPPLY_STATUS_UNKNOWN; } } else { if (power->bat_present == 1) status_changed = 1; power->bat_present = 0; power->bat_status = POWER_SUPPLY_STATUS_UNKNOWN; } power->bat_cap = res->plc[1]; if (status_changed) power_supply_changed(nvec_bat_psy); break; case VOLTAGE: power->bat_voltage_now = res->plu * 1000; break; case TIME_REMAINING: power->time_remain = res->plu * 3600; break; case CURRENT: power->bat_current_now = res->pls * 1000; break; case AVERAGE_CURRENT: power->bat_current_avg = res->pls * 1000; break; case CAPACITY_REMAINING: power->capacity_remain = res->plu * 1000; break; case LAST_FULL_CHARGE_CAPACITY: power->charge_last_full = res->plu * 1000; break; case DESIGN_CAPACITY: power->charge_full_design = res->plu * 1000; break; case CRITICAL_CAPACITY: power->critical_capacity = res->plu * 1000; break; case TEMPERATURE: power->bat_temperature = res->plu - 2732; break; case MANUFACTURER: memcpy(power->bat_manu, &res->plc, res->length - 2); power->bat_model[res->length - 2] = '\0'; break; case MODEL: memcpy(power->bat_model, &res->plc, res->length - 2); power->bat_model[res->length - 2] = '\0'; break; case TYPE: memcpy(power->bat_type, &res->plc, res->length - 2); power->bat_type[res->length - 2] = '\0'; /* * This differs a little from the spec fill in more if you find * some. */ if (!strncmp(power->bat_type, "Li", 30)) power->bat_type_enum = POWER_SUPPLY_TECHNOLOGY_LION; else power->bat_type_enum = POWER_SUPPLY_TECHNOLOGY_UNKNOWN; break; default: return NOTIFY_STOP; } return NOTIFY_STOP; } static int nvec_power_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct nvec_power *power = dev_get_drvdata(psy->dev.parent); switch (psp) { case POWER_SUPPLY_PROP_ONLINE: val->intval = power->on; break; default: return -EINVAL; } return 0; } static int nvec_battery_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { struct nvec_power *power = dev_get_drvdata(psy->dev.parent); switch (psp) { case POWER_SUPPLY_PROP_STATUS: val->intval = power->bat_status; break; case POWER_SUPPLY_PROP_CAPACITY: val->intval = power->bat_cap; break; case POWER_SUPPLY_PROP_PRESENT: val->intval = power->bat_present; break; case POWER_SUPPLY_PROP_VOLTAGE_NOW: val->intval = power->bat_voltage_now; break; case POWER_SUPPLY_PROP_CURRENT_NOW: val->intval = power->bat_current_now; break; case POWER_SUPPLY_PROP_CURRENT_AVG: val->intval = power->bat_current_avg; break; case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: val->intval = power->time_remain; break; case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: val->intval = power->charge_full_design; break; case POWER_SUPPLY_PROP_CHARGE_FULL: val->intval = power->charge_last_full; break; case POWER_SUPPLY_PROP_CHARGE_EMPTY: val->intval = power->critical_capacity; break; case POWER_SUPPLY_PROP_CHARGE_NOW: val->intval = power->capacity_remain; break; case POWER_SUPPLY_PROP_TEMP: val->intval = power->bat_temperature; break; case POWER_SUPPLY_PROP_MANUFACTURER: val->strval = power->bat_manu; break; case POWER_SUPPLY_PROP_MODEL_NAME: val->strval = power->bat_model; break; case POWER_SUPPLY_PROP_TECHNOLOGY: val->intval = power->bat_type_enum; break; default: return -EINVAL; } return 0; } static enum power_supply_property nvec_power_props[] = { POWER_SUPPLY_PROP_ONLINE, }; static enum power_supply_property nvec_battery_props[] = { POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_PRESENT, POWER_SUPPLY_PROP_CAPACITY, POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_CURRENT_NOW, #ifdef EC_FULL_DIAG POWER_SUPPLY_PROP_CURRENT_AVG, POWER_SUPPLY_PROP_TEMP, POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, #endif POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, POWER_SUPPLY_PROP_CHARGE_FULL, POWER_SUPPLY_PROP_CHARGE_EMPTY, POWER_SUPPLY_PROP_CHARGE_NOW, POWER_SUPPLY_PROP_MANUFACTURER, POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_TECHNOLOGY, }; static char *nvec_power_supplied_to[] = { "battery", }; static const struct power_supply_desc nvec_bat_psy_desc = { .name = "battery", .type = POWER_SUPPLY_TYPE_BATTERY, .properties = nvec_battery_props, .num_properties = ARRAY_SIZE(nvec_battery_props), .get_property = nvec_battery_get_property, }; static const struct power_supply_desc nvec_psy_desc = { .name = "ac", .type = POWER_SUPPLY_TYPE_MAINS, .properties = nvec_power_props, .num_properties = ARRAY_SIZE(nvec_power_props), .get_property = nvec_power_get_property, }; static int counter; static const int bat_iter[] = { SLOT_STATUS, VOLTAGE, CURRENT, CAPACITY_REMAINING, #ifdef EC_FULL_DIAG AVERAGE_CURRENT, TEMPERATURE, TIME_REMAINING, #endif }; static void nvec_power_poll(struct work_struct *work) { char buf[] = { NVEC_SYS, GET_SYSTEM_STATUS }; struct nvec_power *power = container_of(work, struct nvec_power, poller.work); if (counter >= ARRAY_SIZE(bat_iter)) counter = 0; /* AC status via sys req */ nvec_write_async(power->nvec, buf, 2); msleep(100); /* * Select a battery request function via round robin doing it all at * once seems to overload the power supply. */ buf[0] = NVEC_BAT; buf[1] = bat_iter[counter++]; nvec_write_async(power->nvec, buf, 2); schedule_delayed_work(to_delayed_work(work), msecs_to_jiffies(5000)); }; static int nvec_power_probe(struct platform_device *pdev) { struct power_supply **psy; const struct power_supply_desc *psy_desc; struct nvec_power *power; struct nvec_chip *nvec = dev_get_drvdata(pdev->dev.parent); struct power_supply_config psy_cfg = {}; power = devm_kzalloc(&pdev->dev, sizeof(struct nvec_power), GFP_NOWAIT); if (!power) return -ENOMEM; dev_set_drvdata(&pdev->dev, power); power->nvec = nvec; switch (pdev->id) { case AC: psy = &nvec_psy; psy_desc = &nvec_psy_desc; psy_cfg.supplied_to = nvec_power_supplied_to; psy_cfg.num_supplicants = ARRAY_SIZE(nvec_power_supplied_to); power->notifier.notifier_call = nvec_power_notifier; INIT_DELAYED_WORK(&power->poller, nvec_power_poll); schedule_delayed_work(&power->poller, msecs_to_jiffies(5000)); break; case BAT: psy = &nvec_bat_psy; psy_desc = &nvec_bat_psy_desc; power->notifier.notifier_call = nvec_power_bat_notifier; break; default: return -ENODEV; } nvec_register_notifier(nvec, &power->notifier, NVEC_SYS); if (pdev->id == BAT) get_bat_mfg_data(power); *psy = power_supply_register(&pdev->dev, psy_desc, &psy_cfg); return PTR_ERR_OR_ZERO(*psy); } static void nvec_power_remove(struct platform_device *pdev) { struct nvec_power *power = platform_get_drvdata(pdev); cancel_delayed_work_sync(&power->poller); nvec_unregister_notifier(power->nvec, &power->notifier); switch (pdev->id) { case AC: power_supply_unregister(nvec_psy); break; case BAT: power_supply_unregister(nvec_bat_psy); } } static struct platform_driver nvec_power_driver = { .probe = nvec_power_probe, .remove_new = nvec_power_remove, .driver = { .name = "nvec-power", } }; module_platform_driver(nvec_power_driver); MODULE_AUTHOR("Ilya Petrov <ilya.muromec@gmail.com>"); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("NVEC battery and AC driver"); MODULE_ALIAS("platform:nvec-power");