| /* |
| * 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> |
| * |
| * This file is subject to the terms and conditions of the GNU General Public |
| * License. See the file "COPYING" in the main directory of this archive |
| * for more details. |
| * |
| */ |
| |
| #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 int const 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 int 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); |
| } |
| |
| return 0; |
| } |
| |
| static struct platform_driver nvec_power_driver = { |
| .probe = nvec_power_probe, |
| .remove = 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"); |