build/patch/kernel/archive/sunxi-6.1/patches.megous/misc-ppkb-manager-Pinephone-Keyboard-power-manager.patch

1001 lines
28 KiB
Diff

From 095e0faf8c00eb1f395a806d20c7cbd37ef80587 Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <megi@xff.cz>
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 <megi@xff.cz>
---
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 <megi@xff.cz>
+ */
+
+#define DEBUG
+
+#include <linux/wait.h>
+#include <linux/device.h>
+#include <linux/debugfs.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/poll.h>
+#include <linux/spinlock.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/regulator/consumer.h>
+#include <linux/delay.h>
+#include <linux/leds.h>
+#include <linux/slab.h>
+#include <linux/time.h>
+#include <linux/reboot.h>
+#include <linux/power_supply.h>
+
+#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 <megi@xff.cz>");
+MODULE_LICENSE("GPL v2");
--
2.35.3