/* 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, }; 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 (%d)\n", props[i].name, ret); return ret; } } *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 (%d)\n", props[i].name, ret); 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); 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; 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\"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); set_bit(KBPWR_F_DISABLED, 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");