build/patch/kernel/archive/sunxi-6.5/patches.megous/misc-modem-power-Power-manager-for-modems.patch

2051 lines
54 KiB
Diff
Raw Normal View History

From e70d0fa0cd39b9c8dfa23adc485087818af4fb85 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Jirman?= <megi@xff.cz>
Date: Sun, 12 Nov 2017 02:10:15 +0100
Subject: [PATCH 175/464] misc: modem-power: Power manager for modems
This driver allows for powering/resetting devices that are otherwise
handled by other subsytems (USB). It also has mechanismsm for polling
from userspace on device->SoC wakeup events via GPIO.
This is mostly useful for controling modems. The supported modems are:
- Quectel EG25
- ZTE MG3732
Signed-off-by: Ondrej Jirman <megi@xff.cz>
---
drivers/misc/Kconfig | 7 +
drivers/misc/Makefile | 1 +
drivers/misc/modem-power.c | 1992 ++++++++++++++++++++++++++++++++++++
3 files changed, 2000 insertions(+)
create mode 100644 drivers/misc/modem-power.c
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 75e427f124b2..76e4eb01236f 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -561,6 +561,13 @@ config TPS6594_PFSM
This driver can also be built as a module. If so, the module
will be called tps6594-pfsm.
+config MODEM_POWER
+ tristate "Modem power/wakeup support for EG25, MG3732, etc."
+ depends on OF && SERIAL_DEV_BUS && RFKILL
+ help
+ This driver provides support for powering up and handling
+ wakeup signals for various modems.
+
source "drivers/misc/c2port/Kconfig"
source "drivers/misc/eeprom/Kconfig"
source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index f2a4d1ff65d4..4a2eb99dc449 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -67,3 +67,4 @@ obj-$(CONFIG_TMR_MANAGER) += xilinx_tmr_manager.o
obj-$(CONFIG_TMR_INJECT) += xilinx_tmr_inject.o
obj-$(CONFIG_TPS6594_ESM) += tps6594-esm.o
obj-$(CONFIG_TPS6594_PFSM) += tps6594-pfsm.o
+obj-$(CONFIG_MODEM_POWER) += modem-power.o
diff --git a/drivers/misc/modem-power.c b/drivers/misc/modem-power.c
new file mode 100644
index 000000000000..5aa1c20f9424
--- /dev/null
+++ b/drivers/misc/modem-power.c
@@ -0,0 +1,1992 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Modem power control driver.
+ *
+ * Ondrej Jirman <megi@xff.cz>
+ *
+ * How this works
+ * --------------
+ *
+ * The driver:
+ * - can be registered as a platform or serial device
+ * - will use gpios, regulator and (optionally) serial port to control the modem
+ * - exposes a character device to control the modem power and receive various
+ * events
+ * - exposes sysfs interface to control modem power and wakeup
+ * - supports multiple modem types and instances
+ *
+ * Power up/power down:
+ * - may take a lot of time (eg. ~13-22s powerup, >22s powerdown)
+ * - happens on a private workqueue under a lock
+ * - may happen from shutdown hook
+ * - prevents suspend when powerup/powerdown is in progress
+ * - is serialized and there's no abort of in-progress operations
+ * - for specific power sequence see comments in the section for each
+ * supported modem variant
+ * - the driver monitors the power status of the modem (optionally)
+ * and tries to complete the powerdown initiated via AT command
+ * - the driver tries to detect when the modem is killswitched off
+ * and updates the driver status to reflect that
+ *
+ * Suspend/resume:
+ * - suspend is blocked if powerup/down is in progress
+ * - modem can wakeup the host over gpio based IRQ (RI signal)
+ * - the driver will assert ap_ready after resume finishes
+ *
+ * Rfkill:
+ * - the driver implements a rfkill interface if rfkill gpio is available
+ */
+
+//#define DEBUG
+
+#include <linux/wait.h>
+#include <linux/interrupt.h>
+#include <linux/device.h>
+#include <linux/cdev.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/slab.h>
+#include <linux/serdev.h>
+#include <linux/rfkill.h>
+
+#define DRIVER_NAME "modem-power"
+
+enum {
+ MPWR_REQ_NONE = 0,
+ MPWR_REQ_RESET,
+ MPWR_REQ_PWDN,
+ MPWR_REQ_PWUP,
+};
+
+enum {
+ MPWR_MODE_NORMAL = 1,
+ MPWR_MODE_DUMB,
+ MPWR_MODE_FASTBOOT,
+ MPWR_MODE_ALT1,
+ MPWR_MODE_ALT2,
+};
+
+struct mpwr_dev;
+
+struct mpwr_gpio {
+ const char* name;
+ unsigned desc_off;
+ int flags;
+ bool required;
+ int irq_flags;
+ unsigned irq_off;
+};
+
+#define MPWR_GPIO_DEF(_name, _flags, _req) \
+ { .name = #_name, \
+ .desc_off = offsetof(struct mpwr_dev, _name##_gpio), \
+ .flags = _flags, \
+ .required = _req, \
+ }
+
+#define MPWR_GPIO_DEF_IRQ(_name, _flags, _req, _irq_flags) \
+ { .name = #_name, \
+ .desc_off = offsetof(struct mpwr_dev, _name##_gpio), \
+ .flags = _flags, \
+ .required = _req, \
+ .irq_flags = _irq_flags, \
+ .irq_off = offsetof(struct mpwr_dev, _name##_irq), \
+ }
+
+struct mpwr_variant {
+ int (*power_init)(struct mpwr_dev* mpwr);
+ int (*power_up)(struct mpwr_dev* mpwr);
+ int (*power_down)(struct mpwr_dev* mpwr);
+ int (*reset)(struct mpwr_dev* mpwr);
+ void (*recv_msg)(struct mpwr_dev *mpwr, const char *msg);
+ int (*suspend)(struct mpwr_dev *mpwr);
+ int (*resume)(struct mpwr_dev *mpwr);
+ const struct mpwr_gpio* gpios;
+ bool regulator_required;
+ bool monitor_wakeup;
+};
+
+struct mpwr_dev {
+ struct device *dev;
+ const struct mpwr_variant* variant;
+
+ wait_queue_head_t wait;
+
+ /* serdev */
+ struct serdev_device *serdev;
+ char rcvbuf[4096];
+ size_t rcvbuf_fill;
+ char msg[4096];
+ int msg_len;
+ int msg_ok;
+ //struct kfifo kfifo;
+ DECLARE_KFIFO(kfifo, unsigned char, 4096);
+
+ /* power */
+ struct regulator *regulator;
+ struct regulator *regulator_vbus;
+
+ /* outputs */
+ struct gpio_desc *enable_gpio;
+ struct gpio_desc *reset_gpio;
+ struct gpio_desc *pwrkey_gpio;
+ struct gpio_desc *sleep_gpio;
+ struct gpio_desc *dtr_gpio;
+ struct gpio_desc *host_ready_gpio;
+ struct gpio_desc *cts_gpio;
+ struct gpio_desc *rts_gpio;
+
+ /* inputs */
+ struct gpio_desc *status_gpio;
+ struct gpio_desc *wakeup_gpio;
+ int wakeup_irq;
+ bool status_pwrkey_multiplexed;
+
+ /* config */
+ struct cdev cdev;
+ dev_t major;
+
+ /* rfkill */
+ struct rfkill *rfkill;
+
+ /* powerup/dn work queue */
+ struct workqueue_struct *wq;
+ struct work_struct power_work;
+ struct work_struct finish_pdn_work;
+ struct mutex modem_lock;
+
+ // change
+ spinlock_t lock; /* protects last_request */
+ int last_request;
+ int powerup_mode;
+ ktime_t last_wakeup;
+
+ struct timer_list wd_timer;
+ struct delayed_work host_ready_work;
+
+ unsigned long flags[1];
+};
+
+enum {
+ /* modem is powered */
+ MPWR_F_POWERED,
+ MPWR_F_POWER_CHANGE_INPROGRESS,
+ MPWR_F_KILLSWITCHED,
+ /* we got a wakeup from the modem */
+ MPWR_F_GOT_WAKEUP,
+ /* serdev */
+ MPWR_F_RECEIVING_MSG,
+ /* eg25 */
+ MPWR_F_GOT_PDN,
+ /* config options */
+ MPWR_F_BLOCKED,
+ /* file */
+ MPWR_F_OPEN,
+ MPWR_F_OVERFLOW,
+};
+
+static struct class* mpwr_class;
+
+static int mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg, int timeout_ms);
+static int mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, int tries);
+static int mpwr_serdev_at_cmd_with_retry_ignore_timeout(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, int tries);
+
+// {{{ mg2723 variant
+
+static int mpwr_mg2723_power_init(struct mpwr_dev* mpwr)
+{
+ // if the device has power applied or doesn't have regulator
+ // configured (we assume it's always powered) initialize GPIO
+ // to shut it down initially
+ if (!mpwr->regulator || regulator_is_enabled(mpwr->regulator)) {
+ gpiod_set_value(mpwr->enable_gpio, 0);
+ gpiod_set_value(mpwr->reset_gpio, 1);
+ } else {
+ // device is not powered, don't drive the gpios
+ gpiod_direction_input(mpwr->enable_gpio);
+ gpiod_direction_input(mpwr->reset_gpio);
+ }
+
+ return 0;
+}
+
+static int mpwr_mg2723_power_up(struct mpwr_dev* mpwr)
+{
+ int ret;
+
+ // power up
+ if (mpwr->regulator) {
+ ret = regulator_enable(mpwr->regulator);
+ if (ret < 0) {
+ dev_err(mpwr->dev,
+ "can't enable power supply err=%d", ret);
+ return ret;
+ }
+ }
+
+ gpiod_direction_output(mpwr->enable_gpio, 1);
+ gpiod_direction_output(mpwr->reset_gpio, 1);
+ msleep(300);
+ gpiod_set_value(mpwr->reset_gpio, 0);
+
+ return 0;
+}
+
+static int mpwr_mg2723_power_down(struct mpwr_dev* mpwr)
+{
+ gpiod_set_value(mpwr->enable_gpio, 0);
+ msleep(50);
+
+ if (mpwr->regulator) {
+ regulator_disable(mpwr->regulator);
+
+ gpiod_direction_input(mpwr->enable_gpio);
+ gpiod_direction_input(mpwr->reset_gpio);
+ } else {
+ gpiod_set_value(mpwr->reset_gpio, 1);
+ }
+
+ return 0;
+}
+
+static int mpwr_mg2723_reset(struct mpwr_dev* mpwr)
+{
+ gpiod_set_value(mpwr->reset_gpio, 1);
+ msleep(300);
+ gpiod_set_value(mpwr->reset_gpio, 0);
+
+ return 0;
+}
+
+static const struct mpwr_gpio mpwr_mg2723_gpios[] = {
+ MPWR_GPIO_DEF(enable, GPIOD_IN, true),
+ MPWR_GPIO_DEF(reset, GPIOD_IN, true),
+ MPWR_GPIO_DEF_IRQ(wakeup, GPIOD_IN, true, IRQF_TRIGGER_FALLING),
+ { },
+};
+
+static const struct mpwr_variant mpwr_mg2723_variant = {
+ .power_init = mpwr_mg2723_power_init,
+ .power_up = mpwr_mg2723_power_up,
+ .power_down = mpwr_mg2723_power_down,
+ .reset = mpwr_mg2723_reset,
+ .gpios = mpwr_mg2723_gpios,
+};
+
+// }}}
+// {{{ eg25 variant
+
+static bool mpwr_eg25_qcfg_airplanecontrol_is_ok(const char* v)
+{
+ return strstarts(v, "1,");
+}
+
+struct mpwr_eg25_qcfg {
+ const char* name;
+ const char* val;
+ bool (*is_ok)(const char* val);
+};
+
+#define EG25G_LATEST_KNOWN_FIRMWARE "EG25GGBR07A08M2G_01.002.07"
+
+static const struct mpwr_eg25_qcfg mpwr_eg25_qcfgs[] = {
+ //{ "risignaltype", "\"respective\"", },
+ { "risignaltype", "\"physical\"", },
+ { "urc/ri/ring", "\"pulse\",1,1000,5000,\"off\",1", },
+ { "urc/ri/smsincoming", "\"pulse\",1,1", },
+ { "urc/ri/other", "\"off\",1,1", },
+ { "urc/ri/pin", "uart_ri", },
+ { "urc/delay", "0", },
+
+ //{ "sleep/datactrl", "0,300,1", },
+
+ { "sleepind/level", "0", },
+ { "wakeupin/level", "0", },
+
+ { "ApRstLevel", "0", },
+ { "ModemRstLevel", "0", },
+
+ // in EG25-G this tries to modify file in /etc (read-only)
+ // and fails
+ //{ "dbgctl", "0", },
+
+ // we don't need AP_READY
+ { "apready", "0,0,500", },
+
+ { "airplanecontrol", "1", mpwr_eg25_qcfg_airplanecontrol_is_ok },
+
+ // available since firmware R07A08_01.002.01.002
+ { "fast/poweroff", "1" },
+};
+
+static char* mpwr_serdev_get_response_value(struct mpwr_dev *mpwr,
+ const char* prefix)
+{
+ int off;
+
+ for (off = 0; off < mpwr->msg_len; off += strlen(mpwr->msg + off) + 1)
+ if (strstarts(mpwr->msg + off, prefix))
+ return mpwr->msg + off + strlen(prefix);
+
+ return NULL;
+}
+
+static struct gpio_desc *mpwr_eg25_get_pwrkey_gpio(struct mpwr_dev *mpwr)
+{
+ if (mpwr->status_pwrkey_multiplexed)
+ return mpwr->status_gpio;
+
+ return mpwr->pwrkey_gpio;
+}
+
+/*
+ * Gpio meanings
+ * -------------
+ *
+ * enable_gpio - 1 = enables RF, 0 = disables RF
+ * sleep_gpio - 1 = puts modem to sleep, 0 = wakes up the modem (must be 0
+ * during poweron)
+ * reset_gpio - accepts 150-460ms reset pulse (high __|^|__)
+ * pwrkey_gpio - accepts 100ms-650ms pulse for powerup (high __|^|__)
+ * 650ms+ pulse for powerdown
+ * (initiated after pulse ends, pulse may have indefinite
+ * duration)
+ * status_gpio - modem power status 0 = powered 1 = unpowered
+ * wakeup_gpio - "ring indicator" output from the modem
+ * host_ready_gpio - AP_READY pin - host is ready to receive URCs
+ *
+ * (pwrkey may be multiplexed with status_gpio)
+ *
+ * Modem behavior
+ * --------------
+ *
+ * wakeup_gpio (RI):
+ * - goes high shortly after power is applied (~15ms)
+ * - goes low when RDY is sent
+ *
+ * dtr_gpio
+ * - when high, modem can sleep if requested
+ * - H->L will wake up a sleeping modem
+ * - internal pull-up
+ *
+ * ri
+ * - pulled low when there's URC
+ * - modem wakes up on URC automatically
+ *
+ * - AT+QURCCFG
+ * - AT+QINDCFG="csq",1
+ * - AT+QINDCFG="ring",1
+ * - AT+QINDCFG="smsincoming",1
+ * - AT+CGREG=0
+ * - AT+CREG=0
+ *
+ * - AT+QURCCFG="urcport","uart1"
+ */
+static int mpwr_eg25_power_up(struct mpwr_dev* mpwr)
+{
+ struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+ bool wakeup_ok, status_ok;
+ bool needs_restart = false;
+ u32 speed = 115200;
+ int ret, i, off;
+ ktime_t start;
+ int mode = mpwr->powerup_mode;
+
+ if (regulator_is_enabled(mpwr->regulator))
+ dev_warn(mpwr->dev,
+ "regulator was already enabled during powerup");
+
+ /* Enable the modem power. */
+ ret = regulator_enable(mpwr->regulator);
+ if (ret < 0) {
+ dev_err(mpwr->dev,
+ "can't enable power supply err=%d", ret);
+ return ret;
+ }
+
+ ret = regulator_enable(mpwr->regulator_vbus);
+ if (ret < 0) {
+ dev_err(mpwr->dev,
+ "can't enable vbus power supply err=%d", ret);
+ regulator_disable(mpwr->regulator);
+ return ret;
+ }
+
+ /* Drive default gpio signals during powerup */
+ /* host_ready_gpio should be 1 during normal powerup */
+ gpiod_direction_output(mpwr->host_ready_gpio, mode != MPWR_MODE_ALT2);
+ /* #W_DISABLE must be left pulled up during modem power up
+ * early on, because opensource bootloader uses this signal to enter
+ * fastboot mode when it's pulled down.
+ *
+ * This should be 1 for normal powerup and 0 for fastboot mode with
+ * special Biktor's firmware.
+ */
+ gpiod_direction_output(mpwr->enable_gpio, mode != MPWR_MODE_FASTBOOT);
+ gpiod_direction_output(mpwr->sleep_gpio, 0);
+ gpiod_direction_output(mpwr->reset_gpio, 0);
+ gpiod_direction_output(pwrkey_gpio, 0);
+ /* dtr_gpio should be 0 during normal powerup */
+ gpiod_direction_output(mpwr->dtr_gpio, mode == MPWR_MODE_ALT1);
+
+ /* Wait for powerup. (30ms min. according to datasheet) */
+ msleep(50);
+
+ /* Send 200ms pwrkey pulse to initiate poweron */
+ gpiod_set_value(pwrkey_gpio, 1);
+ msleep(200);
+ gpiod_set_value(pwrkey_gpio, 0);
+
+ /* skip modem killswitch status checks in fastboot bootloader entry mode */
+ if (mode != MPWR_MODE_NORMAL)
+ goto open_serdev;
+
+ /* Switch status key to input, in case it's multiplexed with pwrkey. */
+ gpiod_direction_input(mpwr->status_gpio);
+
+ /*
+ * Wait for status/wakeup change, assume good values, if CTS/status
+ * signals, are not configured.
+ */
+ status_ok = mpwr->status_gpio ? false : true;
+ wakeup_ok = mpwr->wakeup_gpio ? false : true;
+
+ /* wait up to 10s for status */
+ start = ktime_get();
+ while (ktime_ms_delta(ktime_get(), start) < 10000) {
+ if (!wakeup_ok && mpwr->wakeup_gpio && gpiod_get_value(mpwr->wakeup_gpio)) {
+ dev_info(mpwr->dev, "wakeup ok\n");
+ wakeup_ok = true;
+ }
+
+ if (!status_ok && mpwr->status_gpio && !gpiod_get_value(mpwr->status_gpio)) {
+ dev_info(mpwr->dev, "status ok\n");
+ status_ok = true;
+ }
+
+ /* modem is ready */
+ if (wakeup_ok && status_ok)
+ break;
+
+ msleep(50);
+ }
+
+ if (!wakeup_ok) {
+ dev_err(mpwr->dev, "The modem looks kill-switched\n");
+ if (!test_and_set_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+ sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+ goto err_shutdown_noclose;
+ }
+
+ if (!status_ok) {
+ dev_err(mpwr->dev, "The modem didn't report powerup success in time\n");
+ goto err_shutdown_noclose;
+ }
+
+ if (test_and_clear_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+ sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+
+open_serdev:
+ /* open serial console */
+ ret = serdev_device_open(mpwr->serdev);
+ if (ret) {
+ dev_err(mpwr->dev, "error opening serdev (%d)\n", ret);
+ goto err_shutdown_noclose;
+ }
+
+ of_property_read_u32(mpwr->dev->of_node, "current-speed", &speed);
+ serdev_device_set_baudrate(mpwr->serdev, speed);
+ serdev_device_set_flow_control(mpwr->serdev, false);
+ ret = serdev_device_set_parity(mpwr->serdev, SERDEV_PARITY_NONE);
+ if (ret) {
+ dev_err(mpwr->dev, "error setting serdev parity (%d)\n", ret);
+ goto err_shutdown;
+ }
+
+ if (mode != MPWR_MODE_NORMAL)
+ goto powered_up;
+
+ ret = mpwr_serdev_at_cmd_with_retry_ignore_timeout(mpwr, "AT&FE0", 1000, 30);
+ if (ret)
+ goto err_shutdown;
+
+ /* print firmware version */
+ ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QVERSION;+QSUBSYSVER", 1000, 15);
+ if (ret == 0 && mpwr->msg_len > 0) {
+ bool outdated = false;
+
+ dev_info(mpwr->dev, "===================================================\n");
+ for (off = 0; off < mpwr->msg_len; off += strlen(mpwr->msg + off) + 1) {
+ if (strstr(mpwr->msg + off, "Project Rev") && !strstr(mpwr->msg + off, EG25G_LATEST_KNOWN_FIRMWARE))
+ outdated = true;
+
+ dev_info(mpwr->dev, "%s\n", mpwr->msg + off);
+ }
+ dev_info(mpwr->dev, "===================================================\n");
+
+ if (outdated)
+ dev_warn(mpwr->dev, "Your modem has an outdated firmware. Latest know version is %s. Consider updating.\n", EG25G_LATEST_KNOWN_FIRMWARE);
+ }
+
+ /* print ADB key to dmesg */
+ ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QADBKEY?", 1000, 15);
+ if (ret == 0) {
+ const char *val = mpwr_serdev_get_response_value(mpwr, "+QADBKEY: ");
+ if (val)
+ dev_info(mpwr->dev, "ADB KEY is '%s' (you can use it to unlock ADB access to the modem, see https://xnux.eu/devices/feature/modem-pp.html)\n", val);
+ }
+
+ // check DAI config
+ ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QDAI?", 1000, 15);
+ if (ret == 0) {
+ const char *val = mpwr_serdev_get_response_value(mpwr, "+QDAI: ");
+ const char *needed_val = NULL;
+ char buf[128];
+
+ if (val) {
+ of_property_read_string(mpwr->dev->of_node, "quectel,qdai", &needed_val);
+
+ if (needed_val && strcmp(needed_val, val)) {
+ dev_warn(mpwr->dev, "QDAI is '%s' (changing to '%s')\n", val, needed_val);
+
+ /* update qdai */
+ snprintf(buf, sizeof buf, "AT+QDAI=%s", needed_val);
+ ret = mpwr_serdev_at_cmd(mpwr, buf, 5000);
+ if (ret == 0)
+ needs_restart = true;
+ } else {
+ dev_info(mpwr->dev, "QDAI is '%s'\n", val);
+ }
+ }
+ }
+
+ /* reset the modem, to apply QDAI config if necessary */
+ if (needs_restart) {
+ dev_info(mpwr->dev, "Restarting modem\n");
+
+ /* reboot is broken with fastboot enabled */
+ mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"fast/poweroff\",0", 5000);
+
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+CFUN=1,1", 5000);
+ if (ret)
+ goto err_shutdown;
+
+ /* wait a bit before starting to probe the modem again */
+ msleep(6000);
+
+ ret = mpwr_serdev_at_cmd_with_retry_ignore_timeout(mpwr, "AT&FE0", 1000, 30);
+ if (ret)
+ goto err_shutdown;
+
+ // wait until QDAI starts succeeding (then the modem is ready
+ // to accept the following QCFGs)
+ ret = mpwr_serdev_at_cmd_with_retry(mpwr, "AT+QDAI?", 1000, 15);
+ if (ret)
+ goto err_shutdown;
+ }
+
+ /* check and update important QCFGs */
+ for (i = 0; i < ARRAY_SIZE(mpwr_eg25_qcfgs); i++) {
+ const char* name = mpwr_eg25_qcfgs[i].name;
+ const char* needed_val = mpwr_eg25_qcfgs[i].val;
+ bool (*is_ok)(const char* val) = mpwr_eg25_qcfgs[i].is_ok;
+ const char *val;
+ char buf[128];
+
+ snprintf(buf, sizeof buf, "AT+QCFG=\"%s\"", name);
+ ret = mpwr_serdev_at_cmd(mpwr, buf, 1000);
+ if (ret)
+ continue;
+
+ snprintf(buf, sizeof buf, "+QCFG: \"%s\",", name);
+ val = mpwr_serdev_get_response_value(mpwr, buf);
+ if (val) {
+ if (needed_val && (is_ok ? !is_ok(val) : strcmp(needed_val, val))) {
+ dev_info(mpwr->dev, "QCFG '%s' is '%s' (changing to '%s')\n", name, val, needed_val);
+
+ /* update qcfg */
+ snprintf(buf, sizeof buf, "AT+QCFG=\"%s\",%s", name, needed_val);
+ ret = mpwr_serdev_at_cmd(mpwr, buf, 1000);
+ if (ret)
+ break; /* go to next QCFG */
+ } else {
+ dev_info(mpwr->dev, "QCFG '%s' is '%s'\n", name, val);
+ }
+ }
+ }
+
+ /* setup URC port */
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+QURCCFG=\"urcport\",\"all\"", 2000);
+ if (ret) {
+ dev_info(mpwr->dev, "Your modem doesn't support AT+QURCCFG=\"urcport\",\"all\", consider upgrading the firmware.\n");
+
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+QURCCFG=\"urcport\",\"usbat\"", 2000);
+ if (ret)
+ dev_err(mpwr->dev, "Modem may not report URCs to the right port!\n");
+ }
+
+ /* enable the modem to go to sleep when DTR is low */
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+QSCLK=1", 2000);
+ if (ret)
+ dev_err(mpwr->dev, "Modem will probably not sleep!\n");
+
+powered_up:
+ // if we're signaling some alternate boot mode via GPIO, we need to
+ // sleep here so that modem's boot script notices the gpio
+ if (mode == MPWR_MODE_ALT1 || mode == MPWR_MODE_FASTBOOT || mode == MPWR_MODE_ALT2)
+ msleep(12000);
+
+ gpiod_direction_output(mpwr->dtr_gpio, 1);
+
+ return 0;
+
+err_shutdown:
+ serdev_device_close(mpwr->serdev);
+err_shutdown_noclose:
+ dev_warn(mpwr->dev,
+ "Forcibly cutting off power, data loss may occur.\n");
+ gpiod_direction_input(mpwr->enable_gpio);
+ gpiod_direction_input(mpwr->reset_gpio);
+ gpiod_direction_input(mpwr->sleep_gpio);
+ gpiod_direction_input(pwrkey_gpio);
+ gpiod_direction_input(mpwr->host_ready_gpio);
+ gpiod_direction_input(mpwr->dtr_gpio);
+
+ regulator_disable(mpwr->regulator_vbus);
+ regulator_disable(mpwr->regulator);
+ return -ENODEV;
+}
+
+static int mpwr_eg25_power_down_finish(struct mpwr_dev* mpwr)
+{
+ struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+ ktime_t start = ktime_get();
+ int ret;
+
+ serdev_device_close(mpwr->serdev);
+
+ /*
+ * This function is called right after POWERED DOWN message is received.
+ *
+ * In case of fast/poweroff == 1, no POWERED DOWN message is sent.
+ * Fast power off times are around 1s since the end of 800ms
+ * POK pulse.
+ *
+ * When the modem powers down RI (wakeup) goes low and STATUS goes
+ * high at the same time. Status is not connected on some boards.
+ * RI should be inactive during poweroff, but we don't know for sure.
+ *
+ * Therfore:
+ * - wait for STATUS going low
+ * - in case that's not available wait for RI going low
+ * - in case timings seem off, warn the user
+ *
+ * In addition, some boards have PWRKEY multiplexed with STATUS signal.
+ * In that case we need to switch STATUS to output high level, as soon
+ * as it goes low in order to prevent a power-up signal being registered
+ * by the modem.
+ */
+
+ if (mpwr->status_gpio) {
+ /* wait up to 30s for status going high */
+ while (ktime_ms_delta(ktime_get(), start) < 30000) {
+ if (gpiod_get_value(mpwr->status_gpio)) {
+ if (ktime_ms_delta(ktime_get(), start) < 500)
+ dev_warn(mpwr->dev,
+ "STATUS signal is high too soon during powerdown. Modem is already off?\n");
+ goto powerdown;
+ }
+
+ msleep(20);
+ }
+
+ dev_warn(mpwr->dev,
+ "STATUS signal didn't go high during shutdown. Modem is still on?\n");
+ goto force_powerdown;
+ } else {
+ clear_bit(MPWR_F_GOT_WAKEUP, mpwr->flags);
+
+ if (!gpiod_get_value(mpwr->wakeup_gpio)) {
+ dev_warn(mpwr->dev,
+ "RI signal is low too soon during powerdown. Modem is already off, or spurious wakeup?\n");
+ msleep(2000);
+ goto powerdown;
+ }
+
+ ret = wait_event_timeout(mpwr->wait,
+ test_bit(MPWR_F_GOT_WAKEUP, mpwr->flags),
+ msecs_to_jiffies(30000));
+ if (ret <= 0) {
+ dev_warn(mpwr->dev,
+ "RI signal didn't go low during shutdown, is modem really powering down?\n");
+ goto force_powerdown;
+ }
+
+ if (ktime_ms_delta(ktime_get(), start) < 500) {
+ dev_warn(mpwr->dev,
+ "RI signal is low too soon during powerdown. Modem is already off, or spurious wakeup?\n");
+ msleep(2000);
+ goto powerdown;
+ }
+ }
+
+powerdown:
+ gpiod_direction_input(mpwr->enable_gpio);
+ gpiod_direction_input(mpwr->reset_gpio);
+ gpiod_direction_input(mpwr->sleep_gpio);
+ gpiod_direction_input(pwrkey_gpio);
+ gpiod_direction_input(mpwr->host_ready_gpio);
+ gpiod_direction_input(mpwr->dtr_gpio);
+
+ regulator_disable(mpwr->regulator_vbus);
+ regulator_disable(mpwr->regulator);
+
+ return 0;
+
+force_powerdown:
+ dev_warn(mpwr->dev,
+ "Forcibly cutting off power, data loss may occur.\n");
+ goto powerdown;
+}
+
+static int mpwr_eg25_power_down(struct mpwr_dev* mpwr)
+{
+ struct gpio_desc *pwrkey_gpio = mpwr_eg25_get_pwrkey_gpio(mpwr);
+ //int ret;
+
+ /* Send 800ms pwrkey pulse to initiate powerdown. */
+ gpiod_direction_output(pwrkey_gpio, 1);
+ msleep(800);
+ gpiod_set_value(pwrkey_gpio, 0);
+
+ /* Switch status key to input, in case it's multiplexed with pwrkey. */
+ gpiod_direction_input(mpwr->status_gpio);
+
+ msleep(20);
+
+#if 0
+ // wait for POWERED DOWN message
+ clear_bit(MPWR_F_GOT_PDN, mpwr->flags);
+ ret = wait_event_timeout(mpwr->wait,
+ test_bit(MPWR_F_GOT_PDN, mpwr->flags),
+ msecs_to_jiffies(7000));
+ if (ret <= 0)
+ dev_warn(mpwr->dev,
+ "POWERED DOWN message not received, is modem really powering down?\n");
+#endif
+
+ return mpwr_eg25_power_down_finish(mpwr);
+}
+
+static void mpwr_finish_pdn_work(struct work_struct *work)
+{
+ /*
+ struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, power_work);
+ unsigned long flags;
+
+ spin_lock_irqsave(&mpwr->lock, flags);
+ spin_unlock_irqrestore(&mpwr->lock, flags);
+
+ pm_stay_awake(mpwr->dev);
+
+ mutex_lock(&mpwr->modem_lock);
+
+ mutex_unlock(&mpwr->modem_lock);
+
+ pm_relax(mpwr->dev);
+ */
+}
+
+static void mpwr_eg25_receive_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+ unsigned int msg_len;
+
+ if (!strcmp(msg, "POWERED DOWN")) {
+ // system is powering down
+ set_bit(MPWR_F_GOT_PDN, mpwr->flags);
+ wake_up(&mpwr->wait);
+
+ /*
+ if (mutex_trylock(&mpwr->modem_lock)) {
+ // if no power op is in progress, this means userspace
+ // tried to shut the modem down via AT command, finish up
+ // the job
+
+ pm_stay_awake(mpwr->dev);
+
+ queue_work(mpwr->wq, &mpwr->power_work);
+ dev_warn(mpwr->dev, "userspace shut down the modem via AT command, finishing the job\n");
+ mpwr_eg25_power_down_finish(mpwr);
+ mutex_unlock(&mpwr->modem_lock);
+
+ pm_relax(mpwr->dev);
+ }
+ */
+ return;
+ }
+
+ if (!strcmp(msg, "RDY")) {
+ // system is ready after powerup
+ return;
+ }
+
+ if (!test_bit(MPWR_F_OPEN, mpwr->flags))
+ return;
+
+ msg_len = strlen(msg);
+
+ if (msg_len + 1 > kfifo_avail(&mpwr->kfifo)) {
+ if (!test_and_set_bit(MPWR_F_OVERFLOW, mpwr->flags))
+ wake_up(&mpwr->wait);
+ return;
+ }
+
+ kfifo_in(&mpwr->kfifo, msg, msg_len);
+ kfifo_in(&mpwr->kfifo, "\n", 1);
+ wake_up(&mpwr->wait);
+}
+
+static void mpwr_host_ready_work(struct work_struct *work)
+{
+ struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, host_ready_work.work);
+ int ret;
+
+ mutex_lock(&mpwr->modem_lock);
+ gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+ /*
+ * We need to give the modem some time to wake up.
+ */
+ msleep(5);
+
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"urc/cache\",0", 500);
+ if (ret)
+ dev_warn(mpwr->dev,
+ "Failed to disable urc/cache, you may not be able to see URCs\n");
+
+ gpiod_direction_output(mpwr->dtr_gpio, 1);
+ mutex_unlock(&mpwr->modem_lock);
+
+ gpiod_direction_output(mpwr->host_ready_gpio, 1);
+}
+
+static int mpwr_eg25_suspend(struct mpwr_dev *mpwr)
+{
+ int ret;
+
+ cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+ gpiod_direction_output(mpwr->host_ready_gpio, 0);
+
+ mutex_lock(&mpwr->modem_lock);
+ gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+ msleep(5);
+
+ ret = mpwr_serdev_at_cmd(mpwr, "AT+QCFG=\"urc/cache\",1", 500);
+ if (ret)
+ dev_warn(mpwr->dev,
+ "Failed to enable urc/cache, you may lose URCs during suspend\n");
+
+ gpiod_direction_output(mpwr->dtr_gpio, 1);
+ mutex_unlock(&mpwr->modem_lock);
+
+ return 0;
+}
+
+static int mpwr_eg25_resume(struct mpwr_dev *mpwr)
+{
+ //gpiod_direction_output(mpwr->dtr_gpio, 0);
+
+ // delay disabling URC cache until the whole system is hopefully resumed...
+ schedule_delayed_work(&mpwr->host_ready_work, msecs_to_jiffies(1000));
+
+ return 0;
+}
+
+static const struct mpwr_gpio mpwr_eg25_gpios[] = {
+ MPWR_GPIO_DEF(enable, GPIOD_OUT_HIGH, true),
+ MPWR_GPIO_DEF(reset, GPIOD_OUT_LOW, true),
+ MPWR_GPIO_DEF(pwrkey, GPIOD_OUT_LOW, false),
+ MPWR_GPIO_DEF(dtr, GPIOD_OUT_LOW, true),
+ MPWR_GPIO_DEF(status, GPIOD_IN, false),
+ MPWR_GPIO_DEF_IRQ(wakeup, GPIOD_IN, true,
+ IRQF_TRIGGER_FALLING),
+
+ // XXX: not really needed...
+ MPWR_GPIO_DEF(sleep, GPIOD_OUT_LOW, false),
+ MPWR_GPIO_DEF(host_ready, GPIOD_OUT_HIGH, false),
+ MPWR_GPIO_DEF(cts, GPIOD_IN, false),
+ MPWR_GPIO_DEF(rts, GPIOD_OUT_LOW, false),
+ { },
+};
+
+static const struct mpwr_variant mpwr_eg25_variant = {
+ .power_up = mpwr_eg25_power_up,
+ .power_down = mpwr_eg25_power_down,
+ .recv_msg = mpwr_eg25_receive_msg,
+ .suspend = mpwr_eg25_suspend,
+ .resume = mpwr_eg25_resume,
+ .gpios = mpwr_eg25_gpios,
+ .regulator_required = true,
+ .monitor_wakeup = true,
+};
+
+// }}}
+// {{{ generic helpers
+
+static void mpwr_reset(struct mpwr_dev* mpwr)
+{
+ struct device *dev = mpwr->dev;
+ int ret;
+
+ if (!test_bit(MPWR_F_POWERED, mpwr->flags)) {
+ dev_err(dev, "reset requested but device is not enabled");
+ return;
+ }
+
+ if (!mpwr->reset_gpio) {
+ dev_err(dev, "reset is not configured for this device");
+ return;
+ }
+
+ if (!mpwr->variant->reset) {
+ dev_err(dev, "reset requested but not implemented");
+ return;
+ }
+
+ dev_info(dev, "resetting");
+ ret = mpwr->variant->reset(mpwr);
+ if (ret) {
+ dev_err(dev, "reset failed");
+ }
+}
+
+static void mpwr_power_down(struct mpwr_dev* mpwr)
+{
+ struct device *dev = mpwr->dev;
+ ktime_t start = ktime_get();
+ int ret;
+
+ if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+ return;
+
+ if (!mpwr->variant->power_down) {
+ dev_err(dev, "power down requested but not implemented");
+ return;
+ }
+
+ dev_info(dev, "powering down");
+
+ ret = mpwr->variant->power_down(mpwr);
+ if (ret) {
+ dev_err(dev, "power down failed");
+ } else {
+ clear_bit(MPWR_F_POWERED, mpwr->flags);
+ sysfs_notify(&mpwr->dev->kobj, NULL, "powered");
+ dev_info(mpwr->dev, "powered down in %lld ms\n",
+ ktime_ms_delta(ktime_get(), start));
+ }
+}
+
+static void mpwr_power_up(struct mpwr_dev* mpwr)
+{
+ struct device *dev = mpwr->dev;
+ ktime_t start = ktime_get();
+ int ret;
+
+ if (test_bit(MPWR_F_POWERED, mpwr->flags))
+ return;
+
+ if (!mpwr->variant->power_up) {
+ dev_err(dev, "power up requested but not implemented");
+ return;
+ }
+
+ dev_info(dev, "powering up");
+
+ ret = mpwr->variant->power_up(mpwr);
+ if (ret) {
+ dev_err(dev, "power up failed");
+ } else {
+ set_bit(MPWR_F_POWERED, mpwr->flags);
+ sysfs_notify(&mpwr->dev->kobj, NULL, "powered");
+ dev_info(mpwr->dev, "powered up in %lld ms\n",
+ ktime_ms_delta(ktime_get(), start));
+ }
+}
+
+// }}}
+// {{{ chardev
+
+static int mpwr_release(struct inode *ip, struct file *fp)
+{
+ struct mpwr_dev* mpwr = fp->private_data;
+
+ clear_bit(MPWR_F_OPEN, mpwr->flags);
+
+ return 0;
+}
+
+static int mpwr_open(struct inode *ip, struct file *fp)
+{
+ struct mpwr_dev* mpwr = container_of(ip->i_cdev, struct mpwr_dev, cdev);
+
+ fp->private_data = mpwr;
+
+ if (test_and_set_bit(MPWR_F_OPEN, mpwr->flags))
+ return -EBUSY;
+
+ nonseekable_open(ip, fp);
+ return 0;
+}
+
+static ssize_t mpwr_read(struct file *fp, char __user *buf, size_t len,
+ loff_t *off)
+{
+ struct mpwr_dev* mpwr = fp->private_data;
+ int non_blocking = fp->f_flags & O_NONBLOCK;
+ unsigned int copied;
+ int ret;
+
+ if (non_blocking && kfifo_is_empty(&mpwr->kfifo))
+ return -EWOULDBLOCK;
+
+ ret = wait_event_interruptible(mpwr->wait,
+ !kfifo_is_empty(&mpwr->kfifo)
+ || test_bit(MPWR_F_OVERFLOW, mpwr->flags));
+ if (ret)
+ return ret;
+
+ if (test_and_clear_bit(MPWR_F_OVERFLOW, mpwr->flags)) {
+ if (len < 9)
+ return -E2BIG;
+ if (copy_to_user(buf, "OVERFLOW\n", 9))
+ return -EFAULT;
+ return 9;
+ }
+
+ ret = kfifo_to_user(&mpwr->kfifo, buf, len, &copied);
+
+ return ret ? ret : copied;
+}
+
+static unsigned int mpwr_poll(struct file *fp, poll_table *wait)
+{
+ struct mpwr_dev* mpwr = fp->private_data;
+
+ poll_wait(fp, &mpwr->wait, wait);
+
+ if (!kfifo_is_empty(&mpwr->kfifo))
+ return EPOLLIN | EPOLLRDNORM;
+
+ return 0;
+}
+
+static const struct file_operations mpwr_fops = {
+ .owner = THIS_MODULE,
+ .open = mpwr_open,
+ .release = mpwr_release,
+ .llseek = noop_llseek,
+ .read = mpwr_read,
+ .poll = mpwr_poll,
+};
+
+// }}}
+
+static void mpwr_work_handler(struct work_struct *work)
+{
+ struct mpwr_dev *mpwr = container_of(work, struct mpwr_dev, power_work);
+ unsigned long flags;
+ int last_request;
+
+ spin_lock_irqsave(&mpwr->lock, flags);
+ last_request = mpwr->last_request;
+ mpwr->last_request = 0;
+ spin_unlock_irqrestore(&mpwr->lock, flags);
+
+ pm_stay_awake(mpwr->dev);
+
+ mutex_lock(&mpwr->modem_lock);
+
+ if (last_request == MPWR_REQ_RESET) {
+ mpwr_reset(mpwr);
+ } else if (last_request == MPWR_REQ_PWDN) {
+ mpwr_power_down(mpwr);
+ } else if (last_request == MPWR_REQ_PWUP) {
+ mpwr_power_up(mpwr);
+ }
+
+ mutex_unlock(&mpwr->modem_lock);
+
+ clear_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags);
+ sysfs_notify(&mpwr->dev->kobj, NULL, "is_busy");
+ wake_up(&mpwr->wait);
+
+ pm_relax(mpwr->dev);
+}
+
+static void mpwr_request_power_change(struct mpwr_dev* mpwr, int request, int mode)
+{
+ unsigned long flags;
+
+ set_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags);
+ sysfs_notify(&mpwr->dev->kobj, NULL, "is_busy");
+
+ spin_lock_irqsave(&mpwr->lock, flags);
+ mpwr->last_request = request;
+ if (mode >= 0)
+ mpwr->powerup_mode = mode;
+ spin_unlock_irqrestore(&mpwr->lock, flags);
+
+ queue_work(mpwr->wq, &mpwr->power_work);
+}
+
+static irqreturn_t mpwr_gpio_isr(int irq, void *dev_id)
+{
+ struct mpwr_dev *mpwr = dev_id;
+
+ if (irq == mpwr->wakeup_irq) {
+ dev_dbg(mpwr->dev, "wakeup irq\n");
+
+ if (device_can_wakeup(mpwr->dev))
+ pm_wakeup_event(mpwr->dev, 2000);
+
+ set_bit(MPWR_F_GOT_WAKEUP, mpwr->flags);
+ spin_lock(&mpwr->lock);
+ mpwr->last_wakeup = ktime_get();
+ spin_unlock(&mpwr->lock);
+ wake_up(&mpwr->wait);
+ }
+
+ return IRQ_HANDLED;
+}
+
+static void mpwr_wd_timer_fn(struct timer_list *t)
+{
+ struct mpwr_dev *mpwr = from_timer(mpwr, t, wd_timer);
+
+ if (!mpwr->variant->monitor_wakeup || !test_bit(MPWR_F_POWERED, mpwr->flags))
+ return;
+
+ /*
+ * Monitor wakeup status:
+ *
+ * If RI signal is low for too long we assume the user killswitched
+ * the modem at runtime.
+ */
+ spin_lock(&mpwr->lock);
+ if (!gpiod_get_value(mpwr->wakeup_gpio)) {
+ if (ktime_ms_delta(ktime_get(), mpwr->last_wakeup) > 5000) {
+ if (!test_and_set_bit(MPWR_F_KILLSWITCHED, mpwr->flags))
+ sysfs_notify(&mpwr->dev->kobj, NULL, "killswitched");
+ wake_up(&mpwr->wait);
+ dev_warn(mpwr->dev, "modem looks killswitched at runtime!\n");
+ }
+ }
+ spin_unlock(&mpwr->lock);
+
+ mod_timer(t, jiffies + msecs_to_jiffies(1000));
+}
+
+// {{{ sysfs
+
+static ssize_t powered_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+ return scnprintf(buf, PAGE_SIZE, "%u\n",
+ !!test_bit(MPWR_F_POWERED, mpwr->flags));
+}
+
+static ssize_t powered_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+ unsigned status;
+ int ret;
+
+ if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+ return -EPERM;
+
+ ret = kstrtouint(buf, 10, &status);
+ if (ret)
+ return ret;
+
+ mpwr_request_power_change(mpwr, status ? MPWR_REQ_PWUP : MPWR_REQ_PWDN, status);
+
+ return len;
+}
+
+static ssize_t powered_blocking_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+ unsigned status;
+ int ret;
+
+ if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+ return -EPERM;
+
+ ret = kstrtouint(buf, 10, &status);
+ if (ret)
+ return ret;
+
+ mpwr_request_power_change(mpwr, status ? MPWR_REQ_PWUP : MPWR_REQ_PWDN, status);
+
+ ret = wait_event_interruptible_timeout(mpwr->wait,
+ !test_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags),
+ msecs_to_jiffies(60000));
+ if (ret <= 0) {
+ dev_err(mpwr->dev, "Power state change timeout\n");
+ return -EIO;
+ }
+
+ if (!!status != !!test_bit(MPWR_F_POWERED, mpwr->flags))
+ return -EIO;
+
+ return len;
+}
+
+static ssize_t help_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ return scnprintf(buf, PAGE_SIZE,
+ "echo N > powered, where N can be:\n"
+ "0: power off\n"
+ "1: normal powerup\n"
+ "2: dumb powerup (no AT commands and little error checking during powerup)\n"
+ "3: fastboot powerup (with biktor's patched aboot - #W_DISABLE held low during powerup)\n"
+ "4: alternate powerup (megi's userspace - DTR held high during powerup)\n\n"
+ "echo N > powered_blocking can be used for the write to block until power status transition completes\n");
+}
+
+static ssize_t killswitched_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+ return scnprintf(buf, PAGE_SIZE, "%u\n",
+ !!test_bit(MPWR_F_KILLSWITCHED, mpwr->flags));
+}
+
+static ssize_t is_busy_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+ return scnprintf(buf, PAGE_SIZE, "%u\n",
+ !!test_bit(MPWR_F_POWER_CHANGE_INPROGRESS, mpwr->flags));
+}
+
+static ssize_t hard_reset_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+ bool val;
+ int ret;
+
+ if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+ return -EPERM;
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+ if (val)
+ mpwr_request_power_change(mpwr, MPWR_REQ_RESET, -1);
+
+ return len;
+}
+
+static ssize_t debug_pins_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+ unsigned val;
+ int ret;
+
+ if (test_bit(MPWR_F_BLOCKED, mpwr->flags))
+ return -EPERM;
+
+ ret = kstrtouint(buf, 16, &val);
+ if (ret)
+ return ret;
+
+ gpiod_direction_output(mpwr->host_ready_gpio, val & BIT(0));
+ gpiod_direction_output(mpwr->enable_gpio, val & BIT(1));
+ gpiod_direction_output(mpwr->dtr_gpio, val & BIT(2));
+ gpiod_direction_output(mpwr->rts_gpio, val & BIT(3));
+
+ return len;
+}
+
+static ssize_t debug_pins_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(to_platform_device(dev));
+
+ return scnprintf(buf, PAGE_SIZE, "CTS=%u RI=%u\n",
+ gpiod_get_value(mpwr->cts_gpio), gpiod_get_value(mpwr->wakeup_gpio));
+}
+
+static DEVICE_ATTR_RW(powered);
+static DEVICE_ATTR_WO(powered_blocking);
+static DEVICE_ATTR_RO(killswitched);
+static DEVICE_ATTR_RO(is_busy);
+static DEVICE_ATTR_RO(help);
+static DEVICE_ATTR_WO(hard_reset);
+static DEVICE_ATTR_RW(debug_pins);
+
+static struct attribute *mpwr_attrs[] = {
+ &dev_attr_powered.attr,
+ &dev_attr_powered_blocking.attr,
+ &dev_attr_killswitched.attr,
+ &dev_attr_is_busy.attr,
+ &dev_attr_help.attr,
+ &dev_attr_hard_reset.attr,
+ &dev_attr_debug_pins.attr,
+ NULL,
+};
+
+static const struct attribute_group mpwr_group = {
+ .attrs = mpwr_attrs,
+};
+
+// }}}
+// {{{ rfkill
+
+static int mpwr_rfkill_set(void *data, bool blocked)
+{
+ struct mpwr_dev *mpwr = data;
+
+ gpiod_set_value(mpwr->enable_gpio, !blocked);
+ return 0;
+}
+
+static void mpwr_rfkill_query(struct rfkill *rfkill, void *data)
+{
+ struct mpwr_dev *mpwr = data;
+
+ rfkill_set_sw_state(rfkill, !gpiod_get_value(mpwr->enable_gpio));
+}
+
+static const struct rfkill_ops mpwr_rfkill_ops = {
+ .set_block = mpwr_rfkill_set,
+ .query = mpwr_rfkill_query,
+};
+
+// }}}
+// {{{ probe
+
+static int mpwr_probe_generic(struct device *dev, struct mpwr_dev **mpwr_out)
+{
+ struct mpwr_dev *mpwr;
+ struct device_node *np = dev->of_node;
+ struct device *sdev;
+ const char* cdev_name = NULL;
+ int ret, i;
+
+ mpwr = devm_kzalloc(dev, sizeof(*mpwr), GFP_KERNEL);
+ if (!mpwr)
+ return -ENOMEM;
+
+ mpwr->variant = of_device_get_match_data(dev);
+ if (!mpwr->variant)
+ return -EINVAL;
+
+ mpwr->dev = dev;
+ init_waitqueue_head(&mpwr->wait);
+ mutex_init(&mpwr->modem_lock);
+ spin_lock_init(&mpwr->lock);
+ INIT_WORK(&mpwr->power_work, &mpwr_work_handler);
+ INIT_WORK(&mpwr->finish_pdn_work, &mpwr_finish_pdn_work);
+ INIT_DELAYED_WORK(&mpwr->host_ready_work, mpwr_host_ready_work);
+ INIT_KFIFO(mpwr->kfifo);
+
+ ret = of_property_read_string(np, "char-device-name", &cdev_name);
+ if (ret) {
+ dev_err(dev, "char-device-name is not configured");
+ return -EINVAL;
+ }
+
+ if (of_property_read_bool(np, "blocked"))
+ set_bit(MPWR_F_BLOCKED, mpwr->flags);
+
+ mpwr->status_pwrkey_multiplexed =
+ of_property_read_bool(np, "status-pwrkey-multiplexed");
+
+ mpwr->regulator = devm_regulator_get_optional(dev, "power");
+ if (IS_ERR(mpwr->regulator)) {
+ ret = PTR_ERR(mpwr->regulator);
+ if (ret != -ENODEV) {
+ dev_err(dev, "can't get power supply err=%d", ret);
+ return ret;
+ }
+
+ mpwr->regulator = NULL;
+ }
+
+ if (!mpwr->regulator && mpwr->variant->regulator_required) {
+ dev_err(dev, "can't get power supply err=%d", -ENODEV);
+ return -ENODEV;
+ }
+
+ mpwr->regulator_vbus = devm_regulator_get(dev, "vbus");
+ if (IS_ERR(mpwr->regulator_vbus))
+ return dev_err_probe(dev, PTR_ERR(mpwr->regulator_vbus),
+ "can't get vbus power supply\n");
+
+ for (i = 0; mpwr->variant->gpios[i].name; i++) {
+ const struct mpwr_gpio *io = &mpwr->variant->gpios[i];
+ struct gpio_desc **desc = (struct gpio_desc **)((u8*)mpwr +
+ io->desc_off);
+ int *irq = (int*)((u8*)mpwr + io->irq_off);
+ char buf[64];
+
+ if (io->required)
+ *desc = devm_gpiod_get(dev, io->name, io->flags);
+ else
+ *desc = devm_gpiod_get_optional(dev, io->name, io->flags);
+
+ if (IS_ERR(*desc)) {
+ dev_err(dev, "can't get %s gpio err=%ld", io->name,
+ PTR_ERR(*desc));
+ return PTR_ERR(*desc);
+ }
+
+ if (!*desc)
+ continue;
+
+ if (io->irq_flags == 0 || io->irq_off == 0)
+ continue;
+
+ *irq = gpiod_to_irq(*desc);
+ if (*irq <= 0) {
+ dev_err(dev, "error converting %s gpio to irq: %d",
+ io->name, ret);
+ return *irq;
+ }
+
+ snprintf(buf, sizeof buf, "modem-%s-gpio", io->name);
+ ret = devm_request_irq(dev, *irq, mpwr_gpio_isr, io->irq_flags,
+ devm_kstrdup(dev, buf, GFP_KERNEL), mpwr);
+ if (ret) {
+ dev_err(dev, "error requesting %s irq: %d",
+ io->name, ret);
+ return ret;
+ }
+ }
+
+ if (mpwr->status_pwrkey_multiplexed && mpwr->pwrkey_gpio) {
+ dev_err(dev, "status and pwrkey are multiplexed, but pwrkey defined\n");
+ return -EINVAL;
+ }
+
+ if (mpwr->status_pwrkey_multiplexed && !mpwr->status_gpio) {
+ dev_err(dev, "status and pwrkey are multiplexed, but status is not defined\n");
+ return -EINVAL;
+ }
+
+ ret = devm_device_add_group(dev, &mpwr_group);
+ if (ret)
+ return ret;
+
+ // create char device
+ ret = alloc_chrdev_region(&mpwr->major, 0, 1, "modem-power");
+ if (ret) {
+ dev_err(dev, "can't allocate chrdev region");
+ goto err_disable_regulator;
+ }
+
+ cdev_init(&mpwr->cdev, &mpwr_fops);
+ mpwr->cdev.owner = THIS_MODULE;
+ ret = cdev_add(&mpwr->cdev, mpwr->major, 1);
+ if (ret) {
+ dev_err(dev, "can't add cdev");
+ goto err_unreg_chrev_region;
+ }
+
+ sdev = device_create(mpwr_class, dev, mpwr->major, mpwr, cdev_name);
+ if (IS_ERR(sdev)) {
+ ret = PTR_ERR(sdev);
+ goto err_del_cdev;
+ }
+
+ if (mpwr->wakeup_irq > 0) {
+ ret = device_init_wakeup(dev, true);
+ if (ret) {
+ dev_err(dev, "failed to init wakeup (%d)\n", ret);
+ goto err_free_dev;
+ }
+ }
+
+ if (mpwr->enable_gpio) {
+ mpwr->rfkill = rfkill_alloc("modem", dev, RFKILL_TYPE_WWAN,
+ &mpwr_rfkill_ops, mpwr);
+ if (!mpwr->rfkill) {
+ dev_err(dev, "failed to alloc rfkill\n");
+ ret = -ENOMEM;
+ goto err_deinit_wakeup;
+ }
+
+ rfkill_init_sw_state(mpwr->rfkill, false);
+
+ ret = rfkill_register(mpwr->rfkill);
+ if (ret) {
+ dev_err(dev, "failed to register rfkill (%d)\n", ret);
+ goto err_free_rfkill;
+ }
+ }
+
+ mpwr->wq = alloc_ordered_workqueue("modem-power", 0);
+ if (!mpwr->wq) {
+ ret = -ENOMEM;
+ dev_err(dev, "failed to allocate workqueue\n");
+ goto err_unreg_rfkill;
+ }
+
+ if (mpwr->variant->power_init)
+ mpwr->variant->power_init(mpwr);
+
+ timer_setup(&mpwr->wd_timer, mpwr_wd_timer_fn, 0);
+ mod_timer(&mpwr->wd_timer, jiffies + msecs_to_jiffies(50));
+
+ dev_info(dev, "modem power manager ready");
+ *mpwr_out = mpwr;
+
+ return 0;
+
+err_unreg_rfkill:
+ if (mpwr->rfkill)
+ rfkill_unregister(mpwr->rfkill);
+err_free_rfkill:
+ if (mpwr->rfkill)
+ rfkill_destroy(mpwr->rfkill);
+err_deinit_wakeup:
+ if (mpwr->wakeup_irq > 0)
+ device_init_wakeup(dev, false);
+err_free_dev:
+ device_destroy(mpwr_class, mpwr->major);
+err_del_cdev:
+ cdev_del(&mpwr->cdev);
+err_unreg_chrev_region:
+ unregister_chrdev(mpwr->major, "modem-power");
+err_disable_regulator:
+ cancel_work_sync(&mpwr->power_work);
+ return ret;
+}
+
+static int mpwr_remove_generic(struct mpwr_dev *mpwr)
+{
+ if (mpwr->rfkill) {
+ rfkill_unregister(mpwr->rfkill);
+ rfkill_destroy(mpwr->rfkill);
+ }
+
+ if (mpwr->wakeup_irq > 0)
+ device_init_wakeup(mpwr->dev, false);
+
+ del_timer_sync(&mpwr->wd_timer);
+ cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+ cancel_work_sync(&mpwr->power_work);
+ destroy_workqueue(mpwr->wq);
+
+ mutex_lock(&mpwr->modem_lock);
+ mpwr_power_down(mpwr);
+ mutex_unlock(&mpwr->modem_lock);
+
+ device_destroy(mpwr_class, mpwr->major);
+ cdev_del(&mpwr->cdev);
+ unregister_chrdev(mpwr->major, "modem-power");
+
+ return 0;
+}
+
+static void mpwr_shutdown_generic(struct mpwr_dev *mpwr)
+{
+ cancel_work_sync(&mpwr->power_work);
+ cancel_delayed_work_sync(&mpwr->host_ready_work);
+
+ mutex_lock(&mpwr->modem_lock);
+ mpwr_power_down(mpwr);
+ mutex_unlock(&mpwr->modem_lock);
+}
+
+// }}}
+// {{{ suspend/resume
+
+static int __maybe_unused mpwr_suspend(struct device *dev)
+{
+ struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+ int ret = 0;
+
+ if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+ return 0;
+
+ //if (mpwr->sleep_gpio)
+ //gpiod_direction_output(mpwr->sleep_gpio, 1);
+
+ if (mpwr->variant->suspend)
+ mpwr->variant->suspend(mpwr);
+
+ if (mpwr->wakeup_irq && device_may_wakeup(mpwr->dev))
+ enable_irq_wake(mpwr->wakeup_irq);
+
+ return ret;
+}
+
+static int __maybe_unused mpwr_resume(struct device *dev)
+{
+ struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+ int ret = 0;
+
+ if (!test_bit(MPWR_F_POWERED, mpwr->flags))
+ return 0;
+
+ //if (mpwr->sleep_gpio)
+ //gpiod_direction_output(mpwr->sleep_gpio, 0);
+
+ if (mpwr->variant->resume)
+ mpwr->variant->resume(mpwr);
+
+ if (mpwr->wakeup_irq && device_may_wakeup(mpwr->dev))
+ disable_irq_wake(mpwr->wakeup_irq);
+
+ return ret;
+}
+
+static const struct dev_pm_ops mpwr_pm_ops = {
+ SET_SYSTEM_SLEEP_PM_OPS(mpwr_suspend, mpwr_resume)
+};
+
+// }}}
+// {{{ serdev
+
+static int mpwr_serdev_send_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+ int ret, len;
+ char buf[128];
+
+ if (!mpwr->serdev)
+ return -ENODEV;
+
+ len = snprintf(buf, sizeof buf, "%s\r\n", msg);
+ if (len >= sizeof buf)
+ return -E2BIG;
+
+ ret = serdev_device_write(mpwr->serdev, buf, len, msecs_to_jiffies(3000));
+ if (ret < len)
+ return -EIO;
+
+ serdev_device_wait_until_sent(mpwr->serdev, msecs_to_jiffies(3000));
+
+ return 0;
+}
+
+static int __mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, bool report_error, bool report_timeout)
+{
+ int ret;
+
+ if (test_and_set_bit(MPWR_F_RECEIVING_MSG, mpwr->flags))
+ return -EBUSY;
+
+ mpwr->msg_len = 0;
+
+ dev_dbg(mpwr->dev, "SEND: %s\n", msg);
+
+ ret = mpwr_serdev_send_msg(mpwr, msg);
+ if (ret) {
+ clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+ dev_err(mpwr->dev, "AT command '%s' can't be sent (%d)\n", msg, ret);
+ return ret;
+ }
+
+ ret = wait_event_interruptible_timeout(mpwr->wait,
+ !test_bit(MPWR_F_RECEIVING_MSG, mpwr->flags),
+ msecs_to_jiffies(timeout_ms));
+ if (ret <= 0) {
+ clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+ if (report_timeout)
+ dev_err(mpwr->dev, "AT command '%s' timed out\n", msg);
+ return ret ? ret : -ETIMEDOUT;
+ }
+
+ if (!mpwr->msg_ok) {
+ if (report_error)
+ dev_err(mpwr->dev, "AT command '%s' returned ERROR\n", msg);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int mpwr_serdev_at_cmd(struct mpwr_dev *mpwr, const char *msg, int timeout_ms)
+{
+ return __mpwr_serdev_at_cmd(mpwr, msg, timeout_ms, true, true);
+}
+
+static int __mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, int tries, bool ignore_timeout)
+{
+ int ret = 0;
+
+ if (tries < 1)
+ tries = 1;
+
+ while (tries-- > 0) {
+ ret = __mpwr_serdev_at_cmd(mpwr, msg, timeout_ms, false, !ignore_timeout);
+ if (ret != -EINVAL && (!ignore_timeout || ret != -ETIMEDOUT))
+ return ret;
+
+ if (ret != -ETIMEDOUT)
+ msleep(1000);
+ }
+
+ dev_err(mpwr->dev, "AT command '%s' returned ERROR\n", msg);
+ return ret;
+}
+
+static int mpwr_serdev_at_cmd_with_retry(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, int tries)
+{
+ return __mpwr_serdev_at_cmd_with_retry(mpwr, msg, timeout_ms, tries, false);
+}
+
+static int mpwr_serdev_at_cmd_with_retry_ignore_timeout(struct mpwr_dev *mpwr, const char *msg,
+ int timeout_ms, int tries)
+{
+ return __mpwr_serdev_at_cmd_with_retry(mpwr, msg, timeout_ms, tries, true);
+}
+
+static void mpwr_serdev_receive_msg(struct mpwr_dev *mpwr, const char *msg)
+{
+ dev_dbg(mpwr->dev, "RECV: %s\n", msg);
+
+ if (mpwr->variant->recv_msg)
+ mpwr->variant->recv_msg(mpwr, msg);
+
+ if (!test_bit(MPWR_F_RECEIVING_MSG, mpwr->flags))
+ return;
+
+ if (!strcmp(msg, "OK")) {
+ clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+ mpwr->msg_ok = true;
+ wake_up(&mpwr->wait);
+ return;
+ } else if (!strcmp(msg, "ERROR")) {
+ clear_bit(MPWR_F_RECEIVING_MSG, mpwr->flags);
+ mpwr->msg_ok = false;
+ wake_up(&mpwr->wait);
+ return;
+ } else {
+ int len = strlen(msg);
+
+ if (mpwr->msg_len + len + 1 > sizeof(mpwr->msg)) {
+ dev_warn(mpwr->dev, "message buffer overflow, ignoring message\n");
+ return;
+ }
+
+ memcpy(mpwr->msg + mpwr->msg_len, msg, len + 1);
+ mpwr->msg_len += len + 1;
+ }
+}
+
+static int mpwr_serdev_receive_buf(struct serdev_device *serdev,
+ const unsigned char *buf, size_t count)
+{
+ struct mpwr_dev *mpwr = serdev_device_get_drvdata(serdev);
+ size_t avail = sizeof(mpwr->rcvbuf) - mpwr->rcvbuf_fill;
+ char* p;
+
+ if (avail < count)
+ count = avail;
+
+ if (avail > 0) {
+ memcpy(mpwr->rcvbuf + mpwr->rcvbuf_fill, buf, count);
+ mpwr->rcvbuf_fill += count;
+ }
+
+ while (true) {
+ p = strnstr(mpwr->rcvbuf, "\r\n", mpwr->rcvbuf_fill);
+ if (p) {
+ if (p > mpwr->rcvbuf) {
+ *p = 0;
+ mpwr_serdev_receive_msg(mpwr, mpwr->rcvbuf);
+ }
+
+ mpwr->rcvbuf_fill -= (p - mpwr->rcvbuf) + 2;
+ memmove(mpwr->rcvbuf, p + 2, mpwr->rcvbuf_fill);
+ } else {
+ if (sizeof(mpwr->rcvbuf) - mpwr->rcvbuf_fill == 0) {
+ mpwr->rcvbuf_fill = 0;
+ dev_warn(mpwr->dev, "rcvbuf overflow\n");
+ }
+
+ break;
+ }
+ }
+
+ return count;
+}
+
+static const struct serdev_device_ops mpwr_serdev_ops = {
+ .receive_buf = mpwr_serdev_receive_buf,
+ .write_wakeup = serdev_device_write_wakeup,
+};
+
+static int mpwr_serdev_probe(struct serdev_device *serdev)
+{
+ struct device *dev = &serdev->dev;
+ struct mpwr_dev* mpwr;
+ int ret;
+
+ ret = mpwr_probe_generic(dev, &mpwr);
+ if (ret)
+ return ret;
+
+ serdev_device_set_drvdata(serdev, mpwr);
+ serdev_device_set_client_ops(serdev, &mpwr_serdev_ops);
+ mpwr->serdev = serdev;
+
+ return 0;
+}
+
+static void mpwr_serdev_remove(struct serdev_device *serdev)
+{
+ struct mpwr_dev *mpwr = serdev_device_get_drvdata(serdev);
+
+ mpwr_remove_generic(mpwr);
+}
+
+static const struct of_device_id mpwr_of_match_serdev[] = {
+ { .compatible = "quectel,eg25",
+ .data = &mpwr_eg25_variant },
+ {},
+};
+MODULE_DEVICE_TABLE(of, mpwr_of_match_serdev);
+
+static void mpwr_serdev_shutdown(struct device *dev)
+{
+ struct mpwr_dev *mpwr = dev_get_drvdata(dev);
+
+ mpwr_shutdown_generic(mpwr);
+}
+
+static struct serdev_device_driver mpwr_serdev_driver = {
+ .probe = mpwr_serdev_probe,
+ .remove = mpwr_serdev_remove,
+ .driver = {
+ .name = DRIVER_NAME,
+ .of_match_table = mpwr_of_match_serdev,
+ .pm = &mpwr_pm_ops,
+ .shutdown = mpwr_serdev_shutdown,
+ },
+};
+
+// }}}
+// {{{ platdev
+
+static int mpwr_pdev_probe(struct platform_device *pdev)
+{
+ struct mpwr_dev* mpwr;
+ int ret;
+
+ ret = mpwr_probe_generic(&pdev->dev, &mpwr);
+ if (ret)
+ return ret;
+
+ platform_set_drvdata(pdev, mpwr);
+ return 0;
+}
+
+static int mpwr_pdev_remove(struct platform_device *pdev)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(pdev);
+
+ return mpwr_remove_generic(mpwr);
+}
+
+static void mpwr_pdev_shutdown(struct platform_device *pdev)
+{
+ struct mpwr_dev *mpwr = platform_get_drvdata(pdev);
+
+ mpwr_shutdown_generic(mpwr);
+}
+
+static const struct of_device_id mpwr_of_match_plat[] = {
+ { .compatible = "zte,mg3732",
+ .data = &mpwr_mg2723_variant },
+ {},
+};
+MODULE_DEVICE_TABLE(of, mpwr_of_match_plat);
+
+static struct platform_driver mpwr_platform_driver = {
+ .probe = mpwr_pdev_probe,
+ .remove = mpwr_pdev_remove,
+ .shutdown = mpwr_pdev_shutdown,
+ .driver = {
+ .name = DRIVER_NAME,
+ .of_match_table = mpwr_of_match_plat,
+ .pm = &mpwr_pm_ops,
+ },
+};
+
+// }}}
+// {{{ driver init
+
+static int __init mpwr_driver_init(void)
+{
+ int ret;
+
+ mpwr_class = class_create("modem-power");
+ if (IS_ERR(mpwr_class))
+ return PTR_ERR(mpwr_class);
+
+ ret = serdev_device_driver_register(&mpwr_serdev_driver);
+ if (ret)
+ goto err_class;
+
+ ret = platform_driver_register(&mpwr_platform_driver);
+ if (ret)
+ goto err_serdev;
+
+ return ret;
+
+err_serdev:
+ serdev_device_driver_unregister(&mpwr_serdev_driver);
+err_class:
+ class_destroy(mpwr_class);
+ return ret;
+}
+
+static void __exit mpwr_driver_exit(void)
+{
+ serdev_device_driver_unregister(&mpwr_serdev_driver);
+ platform_driver_unregister(&mpwr_platform_driver);
+ class_destroy(mpwr_class);
+}
+
+module_init(mpwr_driver_init);
+module_exit(mpwr_driver_exit);
+
+MODULE_DESCRIPTION("Modem power manager");
+MODULE_AUTHOR("Ondrej Jirman <megi@xff.cz>");
+MODULE_LICENSE("GPL v2");
+
+// }}}
--
2.34.1