1001 lines
28 KiB
Diff
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
|
|
|