From 095e0faf8c00eb1f395a806d20c7cbd37ef80587 Mon Sep 17 00:00:00 2001 From: Ondrej Jirman Date: Fri, 1 Apr 2022 22:00:11 +0200 Subject: [PATCH 326/388] misc: ppkb-manager: Pinephone Keyboard power manager This commit adds support for in-kernel power management of Pinephone Keyboard for Pinephone and Pinephone Pro. Signed-off-by: Ondrej Jirman --- drivers/misc/Kconfig | 7 + drivers/misc/Makefile | 1 + drivers/misc/ppkb-manager.c | 945 ++++++++++++++++++++++++++++++++++++ 3 files changed, 953 insertions(+) create mode 100644 drivers/misc/ppkb-manager.c diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 2c68a55a7..a798f69af 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -483,6 +483,13 @@ config OPEN_DICE If unsure, say N. +config PPKB_POWER_MANAGER + tristate "Power manager for Pinephone keyboard." + depends on OF + help + This driver coordinates Pinephone keyboard power use between Pinephone + keyboard battery and Pinephone battery. + config VCPU_STALL_DETECTOR tristate "Guest vCPU stall detector" depends on OF && HAS_IOMEM diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index 4ece4e588..809040037 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -60,6 +60,7 @@ obj-$(CONFIG_XILINX_SDFEC) += xilinx_sdfec.o obj-$(CONFIG_HISI_HIKEY_USB) += hisi_hikey_usb.o obj-$(CONFIG_HI6421V600_IRQ) += hi6421v600-irq.o obj-$(CONFIG_OPEN_DICE) += open-dice.o +obj-$(CONFIG_PPKB_POWER_MANAGER)+= ppkb-manager.o obj-$(CONFIG_GP_PCI1XXXX) += mchp_pci1xxxx/ obj-$(CONFIG_VCPU_STALL_DETECTOR) += vcpu_stall_detector.o obj-$(CONFIG_MODEM_POWER) += modem-power.o diff --git a/drivers/misc/ppkb-manager.c b/drivers/misc/ppkb-manager.c new file mode 100644 index 000000000..d705a0ab0 --- /dev/null +++ b/drivers/misc/ppkb-manager.c @@ -0,0 +1,945 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Pinephone keyboard power manager driver. + * + * Ondrej Jirman + */ + +#define DEBUG + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "ppkb-power" + +enum { + KBPWR_F_DISABLED, + KBPWR_F_EMERGENCY_SHUTDOWN, + KBPWR_F_BLOCKED, +}; + +enum { + KBPWR_LED_TRIGGER_KB_VOUT_ON, + KBPWR_LED_TRIGGER_KB_VIN_PRESENT, + KBPWR_LED_TRIGGER_KB_OFFLINE, + KBPWR_LED_TRIGGER_CAPACITY, + + KBPWR_LED_TRIGGER_COUNT, +}; + +static const char *trig_names[] = { + "kbpwr-kb-vout-on", + "kbpwr-kb-vin-present", + "kbpwr-kb-offline", + "kbpwr-capacity", +}; + +struct kbpwr_status { + int kb_cap; // capacity in % (when -1, keyboard charger is + // not accessible, and no kb_* properties are valid) + int kb_cur; // current + charging, - discharging + int kb_vol; // voltage at the battery terminals + int kb_vol_ocv; // OCV voltage + int kb_chg_behavior; // (writable) kb battery charger auto=0/inhibited=1 + int kb_cal; // (writable) battery internal resistance calibration value in mOhm + int kb_out; // 5V output enabled/disabled + int kb_in; // supply to VIN is connected + int kb_max_uwh; // kb battery uWh total capacity + + int ph_cap; // capacity in % + int ph_cur; // current (direction determined by ph_chg_status) + int ph_vol; // voltage at the battery terminals + int ph_chg_status; // POWER_SUPPLY_STATUS_CHARGING = charging, + // other statuses = discharging (use it to + // interpret meaning of abs(ph_cur)) + int ph_chg_cur_limit; // (writable) max charging current for phone battery + int ph_chg_behavior; // (writable) phone charger auto=0/inhibited=1 + + int ph_inp_present; // phone USB supply input is present + int ph_inp_en; // (writable) phone USB supply input is used for powering the phone + int ph_inp_limit; // (writable) input current limit on phone's VBUS + int ph_max_uwh; // phone battery uWh total capacity + + ktime_t ts; +}; + +// constants based on device type +struct kbpwr_machine { + int inp_limit_normal; + int inp_limit_mid; + int inp_limit_high; + int chg_limit_high; + int chg_limit_low; + bool (*has_prop)(const char* name); +}; + +struct kbpwr_dev { + struct device *dev; + struct dentry *debug_root; + + unsigned long flags[1]; + struct mutex lock; + + struct power_supply *phone_battery; + struct power_supply *phone_usb; + + struct power_supply *kb_battery; + struct power_supply *kb_boost; + struct power_supply *kb_usb; + + struct workqueue_struct *wq; + struct delayed_work work; + + struct led_trigger trigger[KBPWR_LED_TRIGGER_COUNT]; + + struct kbpwr_status last_status; + ktime_t ph_low_until; + ktime_t shutdown_after; + + // total state of the battery system + int capacity_total_uwh; + int capacity_uwh; + int capacity_pct; + int power_uw; + int time_left; + + // kb rint calibration + ktime_t rint_valid_until; + int kb_vol_now, kb_cur_now, rint; + + const struct kbpwr_machine* mach; +}; + +static bool kbpwr_has_prop_pp(const char* name) +{ + return true; +} + +static bool kbpwr_has_prop_ppp(const char* name) +{ + return strcmp(name, "ph_inp_en"); +} + +static const struct kbpwr_machine kbpwr_pp = { + .inp_limit_normal = 500000, + .inp_limit_mid = 1000000, + .inp_limit_high = 1500000, + .chg_limit_high = 1200000, + .chg_limit_low = 200000, + .has_prop = kbpwr_has_prop_pp, +}; + +static const struct kbpwr_machine kbpwr_ppp = { + .inp_limit_normal = 450000, + .inp_limit_mid = 850000, + .inp_limit_high = 1500000, + .chg_limit_high = 1200000, + .chg_limit_low = 1000000, + .has_prop = kbpwr_has_prop_ppp, +}; + +static void kbpwr_uevent(struct kbpwr_dev *kbpwr, const char* name) +{ + char *env[] = { + "DRIVER=" DRIVER_NAME, + NULL, + NULL, + }; + + env[1] = kasprintf(GFP_KERNEL, "POWER_EVENT=%s", name); + if (!env[1]) + return; + + kobject_uevent_env(&kbpwr->dev->kobj, KOBJ_CHANGE, env); + + kfree(env[1]); +} + +#define STATUS_PROP(member, sup, sup_prop) \ + { &s->member, #member, kbpwr->sup, sup_prop, }, + +static int kbpwr_snaphost(struct kbpwr_dev *kbpwr, struct kbpwr_status* s) +{ + bool kb_fail = false; + int i, j, ret; + struct { + int *out; + const char* name; + struct power_supply *psy; + enum power_supply_property prop; + } props[] = { + STATUS_PROP(kb_cap, kb_battery, POWER_SUPPLY_PROP_CAPACITY) + STATUS_PROP(kb_cur, kb_battery, POWER_SUPPLY_PROP_CURRENT_NOW) + STATUS_PROP(kb_vol, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW) + STATUS_PROP(kb_vol_ocv, kb_battery, POWER_SUPPLY_PROP_VOLTAGE_OCV) + STATUS_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE) + STATUS_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR) + STATUS_PROP(kb_max_uwh, kb_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN) + + STATUS_PROP(kb_out, kb_boost, POWER_SUPPLY_PROP_ONLINE) + + STATUS_PROP(kb_in, kb_usb, POWER_SUPPLY_PROP_PRESENT) + + STATUS_PROP(ph_cap, phone_battery, POWER_SUPPLY_PROP_CAPACITY) + STATUS_PROP(ph_cur, phone_battery, POWER_SUPPLY_PROP_CURRENT_NOW) + STATUS_PROP(ph_vol, phone_battery, POWER_SUPPLY_PROP_VOLTAGE_NOW) + STATUS_PROP(ph_chg_status, phone_battery, POWER_SUPPLY_PROP_STATUS) + STATUS_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT) + STATUS_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR) + STATUS_PROP(ph_max_uwh, phone_battery, POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN) + + STATUS_PROP(ph_inp_present, phone_usb, POWER_SUPPLY_PROP_PRESENT) + STATUS_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE) + STATUS_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT) + }; + + dev_dbg(kbpwr->dev, "snapshot:\n"); + + for (i = 0; i < ARRAY_SIZE(props); i++) { + union power_supply_propval val = {0,}; + + if (!kbpwr->mach->has_prop(props[i].name)) { + *props[i].out = -1; + continue; + } + + /* + * Skip reading kb_* properties after the first failure. + */ + if (strstarts(props[i].name, "kb_") && kb_fail) + continue; + + ret = power_supply_get_property(props[i].psy, props[i].prop, &val); + if (ret) { + /* + * Failure to read kb_* properties is expected and + * common. When it happens, we clear all the kb_ + * properties, so that algorithm behaves as if keyboard + * charger is sleeping. + */ + if (strstarts(props[i].name, "kb_")) { + kb_fail = true; + for (j = 0; j < ARRAY_SIZE(props); j++) + if (strstarts(props[j].name, "kb_")) + *props[j].out = -1; + continue; + } else { + /* + * Other properties should never fail to read, + * so make that a fatal issue. + */ + dev_err(kbpwr->dev, "Can't read %s\n", props[i].name); + return -1; + } + } + + *props[i].out = val.intval; + + dev_dbg(kbpwr->dev, " %s = %d\n", props[i].name, val.intval); + } + + s->ts = ktime_get(); + + return 0; +} + +#define UPDATE_PROP(member, sup, sup_prop) \ + { &prev->member, &cur->member, #member, kbpwr->sup, sup_prop, }, + +static int kbpwr_update(struct kbpwr_dev *kbpwr, + struct kbpwr_status* prev, + struct kbpwr_status* cur) +{ + bool updated = false; + int i, ret; + struct { + int *cmp; + int *out; + const char* name; + struct power_supply *psy; + enum power_supply_property prop; + } props[] = { + UPDATE_PROP(kb_chg_behavior, kb_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR) + UPDATE_PROP(kb_cal, kb_battery, POWER_SUPPLY_PROP_CALIBRATE) + UPDATE_PROP(ph_chg_cur_limit, phone_battery, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT) + UPDATE_PROP(ph_chg_behavior, phone_battery, POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR) + UPDATE_PROP(ph_inp_en, phone_usb, POWER_SUPPLY_PROP_ONLINE) + UPDATE_PROP(ph_inp_limit, phone_usb, POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT) + }; + + // check if there are changes + for (i = 0; i < ARRAY_SIZE(props); i++) { + if (!kbpwr->mach->has_prop(props[i].name)) + continue; + + if (*props[i].out != *props[i].cmp) { + dev_dbg(kbpwr->dev, "updating:\n"); + break; + } + } + + for (i = 0; i < ARRAY_SIZE(props); i++) { + union power_supply_propval val = {0,}; + + if (!kbpwr->mach->has_prop(props[i].name)) + continue; + + if (*props[i].out == *props[i].cmp) + continue; + + val.intval = *props[i].out; + + /* + * Error handling here is "do as much as we can". Any write + * issue will hopefully be corrected on the next iteration + * of the polling algorithm. + */ + ret = power_supply_set_property(props[i].psy, props[i].prop, &val); + if (ret) { + dev_warn(kbpwr->dev, "Can't write %s\n", props[i].name); + continue; + } + + updated = true; + dev_dbg(kbpwr->dev, " %s = %d\n", props[i].name, val.intval); + } + + if (updated) + kbpwr_uevent(kbpwr, "update"); + + return 0; +} + +static int kbpwr_handle_critical(struct kbpwr_dev *kbpwr) +{ + kbpwr_uevent(kbpwr, "critical"); + + if (!kbpwr->shutdown_after) + kbpwr->shutdown_after = ktime_add_ms(ktime_get(), 60000); + + if (ktime_after(ktime_get(), kbpwr->shutdown_after)) { + dev_emerg(kbpwr->dev, + "critically low capacity reached\n"); + + //hw_protection_shutdown("Critical capacity", 30000); + //set_bit(KBPWR_F_BLOCKED, kbpwr->flags); + return true; + } + + return false; +} + +static void kbpwr_work(struct work_struct *work) +{ + struct kbpwr_dev *kbpwr = container_of(work, struct kbpwr_dev, work.work); + unsigned long delay_on, delay_off; + struct kbpwr_status cur, upd, prev; + int ret; + + if (test_bit(KBPWR_F_DISABLED, kbpwr->flags)) + return; + if (test_bit(KBPWR_F_BLOCKED, kbpwr->flags)) + return; + + mutex_lock(&kbpwr->lock); + + ret = kbpwr_snaphost(kbpwr, &cur); + if (ret) + goto out_try_later; + + prev = kbpwr->last_status.ts ? kbpwr->last_status : cur; + upd = cur; + kbpwr->last_status = cur; + + /* + * We calculate keyboard battery internal resistance based on captured + * keyboard current/voltage at two differnt times after the current + * changes by a largish degree. + */ + if (!kbpwr->rint_valid_until || + ktime_after(ktime_get(), kbpwr->rint_valid_until)) { + kbpwr->kb_vol_now = cur.kb_vol; + kbpwr->kb_cur_now = cur.kb_cur; + kbpwr->rint_valid_until = ktime_add_ms(ktime_get(), + 5 * 60 * 1000); + } else { + s64 diff_vol = cur.kb_vol - kbpwr->kb_vol_now; + s64 diff_cur = cur.kb_cur - kbpwr->kb_cur_now; + + if (abs(diff_cur) > 150000) { + s64 rint = diff_vol * 1000 / diff_cur; + if (rint > 30 && rint < 1000) { + dev_warn(kbpwr->dev, + "calibrating rint=%lld mOhm\n", rint); + kbpwr->rint = rint; + upd.kb_cal = rint; + } + + kbpwr->kb_vol_now = cur.kb_vol; + kbpwr->kb_cur_now = cur.kb_cur; + kbpwr->rint_valid_until = ktime_add_ms(ktime_get(), + 5 * 60 * 1000); + } + } + + /* + * The algorithm here tries to ensure that: + * + * When the power supply is plugged into the keyboard: + * + * 1) Phone's internal battery is charged as fast as possible + * 2) When the internal battery is fully charged, keyboard battery starts charging, while + * still supplying enough power to the phone so that internal battery doesn't start + * discharging, until both batteries are fully charged. + * + * When it's unplugged: + * + * 1) Keyboard battery discharges first, preserving phone battery + * as much as possible. + * 2) Phone battery starts discharging after the keyboard battery + * is emptied. + * + * There are a few corner cases handled: + * + * - It's not a great thing to drain the batteries completely, since Pinephone Pro + * can't recover from this state gracefully, and keyboard also has some issues + * with it, requiring prolonged trickle charging, etc. + * - The driver tries to keep some residual charge in both batteries. + * - On phone power off, keyboard charger output is turned off, so that: + * - Pinephone Pro can be turned off (it can't with voltage present on VBUS) + * - Keyboard battery will not keep charging the phone for no reason. + * - This is only done when the keyboard battery is not plugged in to a power supply. + * - On suspend/resume: + * - Phone battery charger and keyboard battery output are turned off. + * + * LED trigger: + * + * The driver provides a LED trigger to communicate to the user that keyboard + * power button should be pressed to enable the keyboard charger. + */ + + /* check and update the situation */ + + if (cur.kb_cap < 0) { + // keyboard charger is sleeping or no keyboard is detected + + //XXX: check for lack of keyboard + + // restore sane defaults (only sensible if phone is in the + // keyboard) + upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high; + upd.ph_inp_limit = kbpwr->mach->inp_limit_normal; + } else { + // keyboard is connected to the phone + bool kb_in_change = cur.kb_in != prev.kb_in; + + if (cur.kb_in) { + // keyboard is connected to USB PSU (we are charging) + bool kb_chg, ph_chg; + + /* + * Make ph_low comparison stick for 5 minutes in low + * postition, once it crosses the threshold, unless + * kb_in just changed. + */ + bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until); + if (!ph_low) { + ph_low = cur.ph_cap < 80; + if (ph_low) + kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000); + else + kbpwr->ph_low_until = 0; + } + + kb_chg = !ph_low; + ph_chg = ph_low || cur.kb_cap > 90; + + upd.kb_out = 1; + upd.ph_inp_en = 1; + + if (ph_chg) { + // charge the phone + upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_high; + upd.ph_inp_limit = kbpwr->mach->inp_limit_high; + } else { + // supply the phone, but don't charge it + upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + upd.ph_chg_cur_limit = kbpwr->mach->chg_limit_low; + upd.ph_inp_limit = kbpwr->mach->inp_limit_mid; + } + + // charge the keyboard when the KB battery is low or + // phone battery is high + if (kb_chg) { + upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + } else { + upd.kb_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + } + } else { + // keyboard is mobile (we're discharging) + // + // Generally we want to avoid shifting charge between the phone + // and keyboard batteries, so we disable the phone charger in this + // situation and set high input current limit, so that the phone + // is primarily supplied from the keyboard + bool kb_low = cur.kb_vol < 3100000; + + /* + * Make ph_low comparison stick for 5 minutes in low + * postition, once it crosses the threshold, unless + * kb_in just changed. + */ + bool ph_low = kb_in_change ? false : ktime_before(ktime_get(), kbpwr->ph_low_until); + if (!ph_low) { + ph_low = cur.ph_cap < 10; + if (ph_low) + kbpwr->ph_low_until = ktime_add_ms(ktime_get(), 5 * 60000); + else + kbpwr->ph_low_until = 0; + } + + // kb_out + // kb_low ph_low | ph_inp_en ph_chg + // 0 0 | 1 0 + // 0 1 | 1 1 + // 1 1 | 1 1 + // 1 0 | 0 0 + + upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + upd.ph_inp_limit = kbpwr->mach->inp_limit_high; + upd.ph_inp_en = 1; + upd.kb_out = 1; + + // charge phone battery a little if it's charge is too low (we + // need to keep the phone battery somewhat charged at all times, + // if possible) + if (ph_low) { + upd.ph_chg_behavior = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; + } + + if (kb_low && !ph_low) { + upd.ph_inp_en = 0; + upd.kb_out = 0; + upd.ph_inp_limit = kbpwr->mach->inp_limit_normal; + } + } + } + + kbpwr->capacity_total_uwh = cur.ph_max_uwh + + (cur.kb_cap >= 0 ? cur.kb_max_uwh : 0); + kbpwr->capacity_uwh = cur.ph_max_uwh * cur.ph_cap / 100 + + (cur.kb_cap >= 0 ? cur.kb_max_uwh * cur.kb_cap / 100 : 0); + kbpwr->capacity_pct = kbpwr->capacity_uwh / + (kbpwr->capacity_total_uwh / 100); + + kbpwr->power_uw = (cur.ph_cur / 1000) * (cur.ph_vol / 1000); + if (cur.kb_cap >= 0) + kbpwr->power_uw += (cur.kb_cur / 1000) * (cur.kb_vol / 1000); + + kbpwr->time_left = kbpwr->power_uw > 0 ? + kbpwr->capacity_total_uwh - kbpwr->capacity_uwh : + kbpwr->capacity_uwh; + kbpwr->time_left *= 60; + kbpwr->time_left /= abs(kbpwr->power_uw); + + // critical shutdown handler + + if (kbpwr->power_uw < 0 && kbpwr->capacity_pct < 5) { + if (kbpwr_handle_critical(kbpwr)) + goto out_unlock; + } else { + kbpwr->shutdown_after = 0; + } + + // update LED triggers + + // capacity + if (kbpwr->power_uw > 0) { + if (kbpwr->capacity_pct > 95) { + delay_on = 500; delay_off = 0; + } else { + delay_on = delay_off = 500; + } + } else if (kbpwr->capacity_pct < 5) { + delay_on = delay_off = 100; + } else if (kbpwr->capacity_pct < 10) { + delay_on = 100; delay_off = 400; + } else { + delay_on = 0; delay_off = 100; + } + + led_trigger_blink(&kbpwr->trigger[KBPWR_LED_TRIGGER_CAPACITY], + &delay_on, &delay_off); + + led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VOUT_ON], + cur.kb_out > 0 ? LED_FULL : LED_OFF); + + led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_VIN_PRESENT], + cur.kb_in > 0 ? LED_FULL : LED_OFF); + + led_trigger_event(&kbpwr->trigger[KBPWR_LED_TRIGGER_KB_OFFLINE], + cur.kb_cap < 0 ? LED_FULL : LED_OFF); + + kbpwr_update(kbpwr, &cur, &upd); + kbpwr_uevent(kbpwr, "refresh"); + +out_try_later: + queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000)); +out_unlock: + mutex_unlock(&kbpwr->lock); +} + +static ssize_t help_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return scnprintf(buf, PAGE_SIZE, + "Pinephone Keyboard Power Manager\n" + "================================\n" + "disabled - enable/disable the power manager\n" + "shutdown - enable/disable emergency shutdown on low capacity\n" + "help - this help file\n" + ); +} + +static ssize_t disabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev)); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + if (val) { + set_bit(KBPWR_F_DISABLED, kbpwr->flags); + cancel_delayed_work_sync(&kbpwr->work); + kbpwr->ph_low_until = 0; + kbpwr->shutdown_after = 0; + } else { + clear_bit(KBPWR_F_DISABLED, kbpwr->flags); + queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(1000)); + //XXX: do we want to put the system into some particular state? + } + + return len; +} + +static ssize_t disabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev)); + + return scnprintf(buf, PAGE_SIZE, "%d\n", + !!test_bit(KBPWR_F_DISABLED, kbpwr->flags)); +} + +static ssize_t emergency_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev)); + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + if (val) + set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags); + else + clear_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags); + + return len; +} + +static ssize_t emergency_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(to_platform_device(dev)); + + return scnprintf(buf, PAGE_SIZE, "%d\n", + !!test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags)); +} + +static DEVICE_ATTR_RO(help); +static DEVICE_ATTR_RW(disabled); +static DEVICE_ATTR_RW(emergency); + +static struct attribute *kbpwr_attrs[] = { + &dev_attr_help.attr, + &dev_attr_disabled.attr, + &dev_attr_emergency.attr, + NULL, +}; + +static const struct attribute_group kbpwr_group = { + .attrs = kbpwr_attrs, +}; + +static void devm_power_supply_put(struct device *dev, void *res) +{ + struct power_supply **psy = res; + + power_supply_put(*psy); +} + +struct power_supply *devm_power_supply_get_by_name(struct device *dev, + const char *name) +{ + struct power_supply **ptr, *psy; + + ptr = devres_alloc(devm_power_supply_put, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + psy = power_supply_get_by_name(name); + if (IS_ERR_OR_NULL(psy)) { + devres_free(ptr); + } else { + *ptr = psy; + devres_add(dev, ptr); + } + + return psy; +} + +static int kbpwr_status_show(struct seq_file *s, void *data) +{ + struct kbpwr_dev *kbpwr = s->private; + struct kbpwr_status st; + + mutex_lock(&kbpwr->lock); + st = kbpwr->last_status; + mutex_unlock(&kbpwr->lock); + + seq_printf(s, "{\n"); + +#define SHOW_PROP(name) \ + seq_printf(s, "\t\"" #name "\": %d,\n", st.name) + + SHOW_PROP(kb_cap); + SHOW_PROP(kb_cur); + SHOW_PROP(kb_vol); + SHOW_PROP(kb_vol_ocv); + SHOW_PROP(kb_chg_behavior); + SHOW_PROP(kb_cal); + SHOW_PROP(kb_out); + SHOW_PROP(kb_in); + SHOW_PROP(kb_max_uwh); + SHOW_PROP(ph_cap); + SHOW_PROP(ph_cur); + SHOW_PROP(ph_vol); + SHOW_PROP(ph_chg_status); + SHOW_PROP(ph_chg_cur_limit); + SHOW_PROP(ph_chg_behavior); + SHOW_PROP(ph_inp_present); + SHOW_PROP(ph_inp_en); + SHOW_PROP(ph_inp_limit); + SHOW_PROP(ph_max_uwh); + + seq_printf(s, "\t\"disabled\": %s,\n", + test_bit(KBPWR_F_DISABLED, kbpwr->flags) ? "true" : "false"); + seq_printf(s, "\t\"blocked\": %s,\n", + test_bit(KBPWR_F_BLOCKED, kbpwr->flags) ? "true" : "false"); + seq_printf(s, "\t\"emergency_shutdown_enable\": %s,\n", + test_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags) ? "true" : "false"); + + seq_printf(s, "\t\"capacity_total_uwh\": %d,\n", kbpwr->capacity_total_uwh); + seq_printf(s, "\t\"capacity_uwh\": %d,\n", kbpwr->capacity_uwh); + seq_printf(s, "\t\"capacity_pct\": %d,\n", kbpwr->capacity_pct); + seq_printf(s, "\t\"power_uw\": %d,\n", kbpwr->power_uw); + seq_printf(s, "\t\"time_left\": %d,\n", kbpwr->time_left); + + seq_printf(s, "\t\"ts\": %lld\n", st.ts / 1000000); + + seq_printf(s, "}\n"); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(kbpwr_status); + +static int kbpwr_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + struct kbpwr_dev *kbpwr; + int ret, i; + + kbpwr = devm_kzalloc(dev, sizeof(*kbpwr), GFP_KERNEL); + if (!kbpwr) + return -ENOMEM; + + if (of_machine_is_compatible("pine64,pinephone-pro") > 0) { + kbpwr->mach = &kbpwr_ppp; + } else if (of_machine_is_compatible("pine64,pinephone") > 0) { + kbpwr->mach = &kbpwr_pp; + } else { + return dev_err_probe(dev, -EINVAL, "unsupported machine\n"); + } + + kbpwr->dev = dev; + mutex_init(&kbpwr->lock); + INIT_DELAYED_WORK(&kbpwr->work, kbpwr_work); + platform_set_drvdata(pdev, kbpwr); + + struct { + const char* prop; + struct power_supply **psy; + } supplies[] = { + { "phone-battery", &kbpwr->phone_battery, }, + { "phone-usb", &kbpwr->phone_usb, }, + { "kb-battery", &kbpwr->kb_battery, }, + { "kb-boost", &kbpwr->kb_boost, }, + { "kb-usb", &kbpwr->kb_usb, }, + }; + + for (i = 0; i < ARRAY_SIZE(supplies); i++) { + const char* prop = supplies[i].prop; + struct power_supply** psy = supplies[i].psy; + const char* name; + + ret = of_property_read_string(np, prop, &name); + if (ret) + return dev_err_probe(dev, ret, "Can't find supply name for %s\n", prop); + + *psy = devm_power_supply_get_by_name(dev, name); + if (IS_ERR_OR_NULL(*psy)) + return dev_err_probe(dev, -EPROBE_DEFER, + "Couldn't get '%s' power supply\n", name); + } + + ret = devm_device_add_group(dev, &kbpwr_group); + if (ret) + return ret; + + for (i = 0; i < KBPWR_LED_TRIGGER_COUNT; i++) { + kbpwr->trigger[i].name = trig_names[i]; + + ret = devm_led_trigger_register(dev, &kbpwr->trigger[i]); + if (ret) + return dev_err_probe(dev, ret, "failed to register LED trigger %s\n", + kbpwr->trigger[i].name); + } + + kbpwr->wq = alloc_ordered_workqueue("ppkb-power-wq", 0); + if (!kbpwr->wq) + return dev_err_probe(dev, -ENOMEM, "failed to allocate workqueue\n"); + + kbpwr->debug_root = debugfs_create_dir("kbpwr", NULL); + debugfs_create_file("state", 0444, kbpwr->debug_root, kbpwr, + &kbpwr_status_fops); + + dev_info(dev, "Pinephone keyboard power manager ready\n"); + + set_bit(KBPWR_F_EMERGENCY_SHUTDOWN, kbpwr->flags); + if (of_property_read_bool(np, "blocked")) + set_bit(KBPWR_F_BLOCKED, kbpwr->flags); + + queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(10000)); + + return 0; +} + +static int kbpwr_remove(struct platform_device *pdev) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&kbpwr->work); + + mutex_lock(&kbpwr->lock); + //XXX: turn off charging from kb? turn off VOUT if possible + mutex_unlock(&kbpwr->lock); + + destroy_workqueue(kbpwr->wq); + + debugfs_remove(kbpwr->debug_root); + + return 0; +} + +static void kbpwr_shutdown(struct platform_device *pdev) +{ + struct kbpwr_dev *kbpwr = platform_get_drvdata(pdev); + + cancel_delayed_work_sync(&kbpwr->work); + + mutex_lock(&kbpwr->lock); + //XXX: turn off charging from kb? turn off VOUT if possible + mutex_unlock(&kbpwr->lock); +} + +static int __maybe_unused kbpwr_suspend(struct device *dev) +{ + struct kbpwr_dev *kbpwr = dev_get_drvdata(dev); + int ret = 0; + + cancel_delayed_work_sync(&kbpwr->work); + + mutex_lock(&kbpwr->lock); + //XXX: turn off charging from kb? + mutex_unlock(&kbpwr->lock); + + return ret; +} + +static int __maybe_unused kbpwr_resume(struct device *dev) +{ + struct kbpwr_dev *kbpwr = dev_get_drvdata(dev); + int ret = 0; + + //XXX: during quick suspend/resume cycles the work may never run + + // schedule update soon + queue_delayed_work(kbpwr->wq, &kbpwr->work, msecs_to_jiffies(5000)); + + return ret; +} + +static const struct dev_pm_ops kbpwr_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(kbpwr_suspend, kbpwr_resume) +}; + +static const struct of_device_id kbpwr_of_match[] = { + { .compatible = "megi,pinephone-keyboard-power-manager" }, + {}, +}; +MODULE_DEVICE_TABLE(of, kbpwr_of_match); + +static struct platform_driver kbpwr_driver = { + .probe = kbpwr_probe, + .remove = kbpwr_remove, + .shutdown = kbpwr_shutdown, + .driver = { + .name = DRIVER_NAME, + .of_match_table = kbpwr_of_match, + .pm = &kbpwr_pm_ops, + }, +}; + +module_platform_driver(kbpwr_driver); + +MODULE_DESCRIPTION("Pinephone keyboard power manager"); +MODULE_AUTHOR("Ondrej Jirman "); +MODULE_LICENSE("GPL v2"); -- 2.35.3