mirror of
https://github.com/armbian/build.git
synced 2025-09-13 17:51:29 +02:00
I have changed the way the patches are generated a bit. Instead of using orange-pi branch from megous tree for 6.6 kernel, I have used the following kernel branches a83t-suspend, af8133j, anx, audio, axp, cam, drm, err, fixes, mbus, modem, opi3, pb, pinetab, pp, ppkb, samuel, speed, tbs-a711, ths These branches were carefully chosen to include only allwinner related patches and remove importing of the rockchip related patches into the allwinner kernel. Following patches are modified to fix patch application failure - patches.armbian/arm64-dts-sun50i-h616-orangepi-zero2-reg_usb1_vbus-status-ok.patch - patches.armbian/arm64-dts-sun50i-h616-orangepi-zero2-Enable-GPU-mali.patch - patches.armbian/arm64-dts-allwinner-h616-Add-efuse_xlate-cpu-frequency-scaling-v1_6_2.patch - patches.armbian/arm64-dts-allwinner-h616-LED-green_power_on-red_status_heartbeat.patch - patches.armbian/arm64-dts-allwinner-overlay-Add-Overlays-for-sunxi64.patch - patches.armbian/arm64-dts-sun50i-h616-bigtreetech-cb1.patch Following patches are modified because of kernel api change to fix compilation failure - patches.armbian/drv-gpu-drm-sun4i-Add-HDMI-audio-sun4i-hdmi-encoder.patch - patches.armbian/drv-of-Device-Tree-Overlay-ConfigFS-interface.patch
2050 lines
54 KiB
Diff
2050 lines
54 KiB
Diff
From 0000000000000000000000000000000000000000 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: 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(+)
|
|
|
|
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
|
|
index cadd4a820c03..b9d019f69b90 100644
|
|
--- a/drivers/misc/Kconfig
|
|
+++ b/drivers/misc/Kconfig
|
|
@@ -562,6 +562,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");
|
|
+
|
|
+// }}}
|
|
--
|
|
Armbian
|
|
|