2051 lines
54 KiB
Diff
2051 lines
54 KiB
Diff
From f4136a4608202a9b2387e967ff8315c2f6d73364 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 172/389] 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 358ad56f6524..2c68a55a78fa 100644
|
|
--- a/drivers/misc/Kconfig
|
|
+++ b/drivers/misc/Kconfig
|
|
@@ -496,6 +496,13 @@ config VCPU_STALL_DETECTOR
|
|
|
|
If you do not intend to run this kernel as a guest, say N.
|
|
|
|
+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 ac9b3e757ba1..4ece4e588497 100644
|
|
--- a/drivers/misc/Makefile
|
|
+++ b/drivers/misc/Makefile
|
|
@@ -62,3 +62,4 @@ obj-$(CONFIG_HI6421V600_IRQ) += hi6421v600-irq.o
|
|
obj-$(CONFIG_OPEN_DICE) += open-dice.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/modem-power.c b/drivers/misc/modem-power.c
|
|
new file mode 100644
|
|
index 000000000000..16c731df6e23
|
|
--- /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(THIS_MODULE, "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.35.3
|
|
|