// SPDX-License-Identifier: GPL-2.0 /* * Motorola Mapphone MDM6600 modem GPIO controlled USB PHY driver * Copyright (C) 2018 Tony Lindgren */ #include #include #include #include #include #include #include #include #include #include #include #include #define PHY_MDM6600_PHY_DELAY_MS 4000 /* PHY enable 2.2s to 3.5s */ #define PHY_MDM6600_ENABLED_DELAY_MS 8000 /* 8s more total for MDM6600 */ #define PHY_MDM6600_WAKE_KICK_MS 600 /* time on after GPIO toggle */ #define MDM6600_MODEM_IDLE_DELAY_MS 1000 /* modem after USB suspend */ #define MDM6600_MODEM_WAKE_DELAY_MS 200 /* modem response after idle */ enum phy_mdm6600_ctrl_lines { PHY_MDM6600_ENABLE, /* USB PHY enable */ PHY_MDM6600_POWER, /* Device power */ PHY_MDM6600_RESET, /* Device reset */ PHY_MDM6600_NR_CTRL_LINES, }; enum phy_mdm6600_bootmode_lines { PHY_MDM6600_MODE0, /* out USB mode0 and OOB wake */ PHY_MDM6600_MODE1, /* out USB mode1, in OOB wake */ PHY_MDM6600_NR_MODE_LINES, }; enum phy_mdm6600_cmd_lines { PHY_MDM6600_CMD0, PHY_MDM6600_CMD1, PHY_MDM6600_CMD2, PHY_MDM6600_NR_CMD_LINES, }; enum phy_mdm6600_status_lines { PHY_MDM6600_STATUS0, PHY_MDM6600_STATUS1, PHY_MDM6600_STATUS2, PHY_MDM6600_NR_STATUS_LINES, }; /* * MDM6600 command codes. These are based on Motorola Mapphone Linux * kernel tree. */ enum phy_mdm6600_cmd { PHY_MDM6600_CMD_BP_PANIC_ACK, PHY_MDM6600_CMD_DATA_ONLY_BYPASS, /* Reroute USB to CPCAP PHY */ PHY_MDM6600_CMD_FULL_BYPASS, /* Reroute USB to CPCAP PHY */ PHY_MDM6600_CMD_NO_BYPASS, /* Request normal USB mode */ PHY_MDM6600_CMD_BP_SHUTDOWN_REQ, /* Request device power off */ PHY_MDM6600_CMD_BP_UNKNOWN_5, PHY_MDM6600_CMD_BP_UNKNOWN_6, PHY_MDM6600_CMD_UNDEFINED, }; /* * MDM6600 status codes. These are based on Motorola Mapphone Linux * kernel tree. */ enum phy_mdm6600_status { PHY_MDM6600_STATUS_PANIC, /* Seems to be really off */ PHY_MDM6600_STATUS_PANIC_BUSY_WAIT, PHY_MDM6600_STATUS_QC_DLOAD, PHY_MDM6600_STATUS_RAM_DOWNLOADER, /* MDM6600 USB flashing mode */ PHY_MDM6600_STATUS_PHONE_CODE_AWAKE, /* MDM6600 normal USB mode */ PHY_MDM6600_STATUS_PHONE_CODE_ASLEEP, PHY_MDM6600_STATUS_SHUTDOWN_ACK, PHY_MDM6600_STATUS_UNDEFINED, }; static const char * const phy_mdm6600_status_name[] = { "off", "busy", "qc_dl", "ram_dl", "awake", "asleep", "shutdown", "undefined", }; struct phy_mdm6600 { struct device *dev; struct phy *generic_phy; struct phy_provider *phy_provider; struct gpio_desc *ctrl_gpios[PHY_MDM6600_NR_CTRL_LINES]; struct gpio_descs *mode_gpios; struct gpio_descs *status_gpios; struct gpio_descs *cmd_gpios; struct delayed_work bootup_work; struct delayed_work status_work; struct delayed_work modem_wake_work; struct completion ack; bool enabled; /* mdm6600 phy enabled */ bool running; /* mdm6600 boot done */ bool awake; /* mdm6600 respnds on n_gsm */ int status; }; static int phy_mdm6600_init(struct phy *x) { struct phy_mdm6600 *ddata = phy_get_drvdata(x); struct gpio_desc *enable_gpio = ddata->ctrl_gpios[PHY_MDM6600_ENABLE]; if (!ddata->enabled) return -EPROBE_DEFER; gpiod_set_value_cansleep(enable_gpio, 0); return 0; } static int phy_mdm6600_power_on(struct phy *x) { struct phy_mdm6600 *ddata = phy_get_drvdata(x); struct gpio_desc *enable_gpio = ddata->ctrl_gpios[PHY_MDM6600_ENABLE]; int error; if (!ddata->enabled) return -ENODEV; error = pinctrl_pm_select_default_state(ddata->dev); if (error) dev_warn(ddata->dev, "%s: error with default_state: %i\n", __func__, error); gpiod_set_value_cansleep(enable_gpio, 1); /* Allow aggressive PM for USB, it's only needed for n_gsm port */ if (pm_runtime_enabled(&x->dev)) phy_pm_runtime_put(x); return 0; } static int phy_mdm6600_power_off(struct phy *x) { struct phy_mdm6600 *ddata = phy_get_drvdata(x); struct gpio_desc *enable_gpio = ddata->ctrl_gpios[PHY_MDM6600_ENABLE]; int error; if (!ddata->enabled) return -ENODEV; /* Paired with phy_pm_runtime_put() in phy_mdm6600_power_on() */ if (pm_runtime_enabled(&x->dev)) { error = phy_pm_runtime_get(x); if (error < 0 && error != -EINPROGRESS) dev_warn(ddata->dev, "%s: phy_pm_runtime_get: %i\n", __func__, error); } gpiod_set_value_cansleep(enable_gpio, 0); error = pinctrl_pm_select_sleep_state(ddata->dev); if (error) dev_warn(ddata->dev, "%s: error with sleep_state: %i\n", __func__, error); return 0; } static const struct phy_ops gpio_usb_ops = { .init = phy_mdm6600_init, .power_on = phy_mdm6600_power_on, .power_off = phy_mdm6600_power_off, .owner = THIS_MODULE, }; /** * phy_mdm6600_cmd() - send a command request to mdm6600 * @ddata: device driver data * * Configures the three command request GPIOs to the specified value. */ static void phy_mdm6600_cmd(struct phy_mdm6600 *ddata, int val) { DECLARE_BITMAP(values, PHY_MDM6600_NR_CMD_LINES); values[0] = val; gpiod_set_array_value_cansleep(PHY_MDM6600_NR_CMD_LINES, ddata->cmd_gpios->desc, ddata->cmd_gpios->info, values); } /** * phy_mdm6600_status() - read mdm6600 status lines * @ddata: device driver data */ static void phy_mdm6600_status(struct work_struct *work) { struct phy_mdm6600 *ddata; struct device *dev; DECLARE_BITMAP(values, PHY_MDM6600_NR_STATUS_LINES); int error; ddata = container_of(work, struct phy_mdm6600, status_work.work); dev = ddata->dev; error = gpiod_get_array_value_cansleep(PHY_MDM6600_NR_STATUS_LINES, ddata->status_gpios->desc, ddata->status_gpios->info, values); if (error) return; ddata->status = values[0] & ((1 << PHY_MDM6600_NR_STATUS_LINES) - 1); dev_info(dev, "modem status: %i %s\n", ddata->status, phy_mdm6600_status_name[ddata->status]); complete(&ddata->ack); } static irqreturn_t phy_mdm6600_irq_thread(int irq, void *data) { struct phy_mdm6600 *ddata = data; schedule_delayed_work(&ddata->status_work, msecs_to_jiffies(10)); return IRQ_HANDLED; } /** * phy_mdm6600_wakeirq_thread - handle mode1 line OOB wake after booting * @irq: interrupt * @data: interrupt handler data * * GPIO mode1 is used initially as output to configure the USB boot * mode for mdm6600. After booting it is used as input for OOB wake * signal from mdm6600 to the SoC. Just use it for debug info only * for now. */ static irqreturn_t phy_mdm6600_wakeirq_thread(int irq, void *data) { struct phy_mdm6600 *ddata = data; struct gpio_desc *mode_gpio1; int error, wakeup; mode_gpio1 = ddata->mode_gpios->desc[PHY_MDM6600_MODE1]; wakeup = gpiod_get_value(mode_gpio1); if (!wakeup) return IRQ_NONE; dev_dbg(ddata->dev, "OOB wake on mode_gpio1: %i\n", wakeup); error = pm_runtime_get_sync(ddata->dev); if (error < 0) { pm_runtime_put_noidle(ddata->dev); return IRQ_NONE; } /* Just wake-up and kick the autosuspend timer */ pm_runtime_mark_last_busy(ddata->dev); pm_runtime_put_autosuspend(ddata->dev); return IRQ_HANDLED; } /** * phy_mdm6600_init_irq() - initialize mdm6600 status IRQ lines * @ddata: device driver data */ static void phy_mdm6600_init_irq(struct phy_mdm6600 *ddata) { struct device *dev = ddata->dev; int i, error, irq; for (i = PHY_MDM6600_STATUS0; i <= PHY_MDM6600_STATUS2; i++) { struct gpio_desc *gpio = ddata->status_gpios->desc[i]; irq = gpiod_to_irq(gpio); if (irq <= 0) continue; error = devm_request_threaded_irq(dev, irq, NULL, phy_mdm6600_irq_thread, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "mdm6600", ddata); if (error) dev_warn(dev, "no modem status irq%i: %i\n", irq, error); } } struct phy_mdm6600_map { const char *name; int direction; }; static const struct phy_mdm6600_map phy_mdm6600_ctrl_gpio_map[PHY_MDM6600_NR_CTRL_LINES] = { { "enable", GPIOD_OUT_LOW, }, /* low = phy disabled */ { "power", GPIOD_OUT_LOW, }, /* low = off */ { "reset", GPIOD_OUT_HIGH, }, /* high = reset */ }; /** * phy_mdm6600_init_lines() - initialize mdm6600 GPIO lines * @ddata: device driver data */ static int phy_mdm6600_init_lines(struct phy_mdm6600 *ddata) { struct device *dev = ddata->dev; int i; /* MDM6600 control lines */ for (i = 0; i < ARRAY_SIZE(phy_mdm6600_ctrl_gpio_map); i++) { const struct phy_mdm6600_map *map = &phy_mdm6600_ctrl_gpio_map[i]; struct gpio_desc **gpio = &ddata->ctrl_gpios[i]; *gpio = devm_gpiod_get(dev, map->name, map->direction); if (IS_ERR(*gpio)) { dev_info(dev, "gpio %s error %li\n", map->name, PTR_ERR(*gpio)); return PTR_ERR(*gpio); } } /* MDM6600 USB start-up mode output lines */ ddata->mode_gpios = devm_gpiod_get_array(dev, "motorola,mode", GPIOD_OUT_LOW); if (IS_ERR(ddata->mode_gpios)) return PTR_ERR(ddata->mode_gpios); if (ddata->mode_gpios->ndescs != PHY_MDM6600_NR_MODE_LINES) return -EINVAL; /* MDM6600 status input lines */ ddata->status_gpios = devm_gpiod_get_array(dev, "motorola,status", GPIOD_IN); if (IS_ERR(ddata->status_gpios)) return PTR_ERR(ddata->status_gpios); if (ddata->status_gpios->ndescs != PHY_MDM6600_NR_STATUS_LINES) return -EINVAL; /* MDM6600 cmd output lines */ ddata->cmd_gpios = devm_gpiod_get_array(dev, "motorola,cmd", GPIOD_OUT_LOW); if (IS_ERR(ddata->cmd_gpios)) return PTR_ERR(ddata->cmd_gpios); if (ddata->cmd_gpios->ndescs != PHY_MDM6600_NR_CMD_LINES) return -EINVAL; return 0; } /** * phy_mdm6600_device_power_on() - power on mdm6600 device * @ddata: device driver data * * To get the integrated USB phy in MDM6600 takes some hoops. We must ensure * the shared USB bootmode GPIOs are configured, then request modem start-up, * reset and power-up.. And then we need to recycle the shared USB bootmode * GPIOs as they are also used for Out of Band (OOB) wake for the USB and * TS 27.010 serial mux. */ static int phy_mdm6600_device_power_on(struct phy_mdm6600 *ddata) { struct gpio_desc *mode_gpio0, *mode_gpio1, *reset_gpio, *power_gpio; int error = 0, wakeirq; mode_gpio0 = ddata->mode_gpios->desc[PHY_MDM6600_MODE0]; mode_gpio1 = ddata->mode_gpios->desc[PHY_MDM6600_MODE1]; reset_gpio = ddata->ctrl_gpios[PHY_MDM6600_RESET]; power_gpio = ddata->ctrl_gpios[PHY_MDM6600_POWER]; /* * Shared GPIOs must be low for normal USB mode. After booting * they are used for OOB wake signaling. These can be also used * to configure USB flashing mode later on based on a module * parameter. */ gpiod_set_value_cansleep(mode_gpio0, 0); gpiod_set_value_cansleep(mode_gpio1, 0); /* Request start-up mode */ phy_mdm6600_cmd(ddata, PHY_MDM6600_CMD_NO_BYPASS); /* Request a reset first */ gpiod_set_value_cansleep(reset_gpio, 0); msleep(100); /* Toggle power GPIO to request mdm6600 to start */ gpiod_set_value_cansleep(power_gpio, 1); msleep(100); gpiod_set_value_cansleep(power_gpio, 0); /* * Looks like the USB PHY needs between 2.2 to 4 seconds. * If we try to use it before that, we will get L3 errors * from omap-usb-host trying to access the PHY. See also * phy_mdm6600_init() for -EPROBE_DEFER. */ msleep(PHY_MDM6600_PHY_DELAY_MS); ddata->enabled = true; /* Booting up the rest of MDM6600 will take total about 8 seconds */ dev_info(ddata->dev, "Waiting for power up request to complete..\n"); if (wait_for_completion_timeout(&ddata->ack, msecs_to_jiffies(PHY_MDM6600_ENABLED_DELAY_MS))) { if (ddata->status > PHY_MDM6600_STATUS_PANIC && ddata->status < PHY_MDM6600_STATUS_SHUTDOWN_ACK) dev_info(ddata->dev, "Powered up OK\n"); } else { ddata->enabled = false; error = -ETIMEDOUT; dev_err(ddata->dev, "Timed out powering up\n"); } /* Reconfigure mode1 GPIO as input for OOB wake */ gpiod_direction_input(mode_gpio1); wakeirq = gpiod_to_irq(mode_gpio1); if (wakeirq <= 0) return wakeirq; error = devm_request_threaded_irq(ddata->dev, wakeirq, NULL, phy_mdm6600_wakeirq_thread, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "mdm6600-wake", ddata); if (error) dev_warn(ddata->dev, "no modem wakeirq irq%i: %i\n", wakeirq, error); ddata->running = true; return error; } /** * phy_mdm6600_device_power_off() - power off mdm6600 device * @ddata: device driver data */ static void phy_mdm6600_device_power_off(struct phy_mdm6600 *ddata) { struct gpio_desc *reset_gpio = ddata->ctrl_gpios[PHY_MDM6600_RESET]; ddata->enabled = false; phy_mdm6600_cmd(ddata, PHY_MDM6600_CMD_BP_SHUTDOWN_REQ); msleep(100); gpiod_set_value_cansleep(reset_gpio, 1); dev_info(ddata->dev, "Waiting for power down request to complete.. "); if (wait_for_completion_timeout(&ddata->ack, msecs_to_jiffies(5000))) { if (ddata->status == PHY_MDM6600_STATUS_PANIC) dev_info(ddata->dev, "Powered down OK\n"); } else { dev_err(ddata->dev, "Timed out powering down\n"); } } static void phy_mdm6600_deferred_power_on(struct work_struct *work) { struct phy_mdm6600 *ddata; int error; ddata = container_of(work, struct phy_mdm6600, bootup_work.work); error = phy_mdm6600_device_power_on(ddata); if (error) dev_err(ddata->dev, "Device not functional\n"); } /* * USB suspend puts mdm6600 into low power mode. For any n_gsm using apps, * we need to keep the modem awake by kicking it's mode0 GPIO. This will * keep the modem awake for about 1.2 seconds. When no n_gsm apps are using * the modem, runtime PM auto mode can be enabled so modem can enter low * power mode. */ static void phy_mdm6600_wake_modem(struct phy_mdm6600 *ddata) { struct gpio_desc *mode_gpio0; mode_gpio0 = ddata->mode_gpios->desc[PHY_MDM6600_MODE0]; gpiod_set_value_cansleep(mode_gpio0, 1); usleep_range(5, 15); gpiod_set_value_cansleep(mode_gpio0, 0); if (ddata->awake) usleep_range(5, 15); else msleep(MDM6600_MODEM_WAKE_DELAY_MS); } static void phy_mdm6600_modem_wake(struct work_struct *work) { struct phy_mdm6600 *ddata; ddata = container_of(work, struct phy_mdm6600, modem_wake_work.work); phy_mdm6600_wake_modem(ddata); /* * The modem does not always stay awake 1.2 seconds after toggling * the wake GPIO, and sometimes it idles after about some 600 ms * making writes time out. */ schedule_delayed_work(&ddata->modem_wake_work, msecs_to_jiffies(PHY_MDM6600_WAKE_KICK_MS)); } static int __maybe_unused phy_mdm6600_runtime_suspend(struct device *dev) { struct phy_mdm6600 *ddata = dev_get_drvdata(dev); cancel_delayed_work_sync(&ddata->modem_wake_work); ddata->awake = false; return 0; } static int __maybe_unused phy_mdm6600_runtime_resume(struct device *dev) { struct phy_mdm6600 *ddata = dev_get_drvdata(dev); phy_mdm6600_modem_wake(&ddata->modem_wake_work.work); ddata->awake = true; return 0; } static const struct dev_pm_ops phy_mdm6600_pm_ops = { SET_RUNTIME_PM_OPS(phy_mdm6600_runtime_suspend, phy_mdm6600_runtime_resume, NULL) }; static const struct of_device_id phy_mdm6600_id_table[] = { { .compatible = "motorola,mapphone-mdm6600", }, {}, }; MODULE_DEVICE_TABLE(of, phy_mdm6600_id_table); static int phy_mdm6600_probe(struct platform_device *pdev) { struct phy_mdm6600 *ddata; int error; ddata = devm_kzalloc(&pdev->dev, sizeof(*ddata), GFP_KERNEL); if (!ddata) return -ENOMEM; INIT_DELAYED_WORK(&ddata->bootup_work, phy_mdm6600_deferred_power_on); INIT_DELAYED_WORK(&ddata->status_work, phy_mdm6600_status); INIT_DELAYED_WORK(&ddata->modem_wake_work, phy_mdm6600_modem_wake); init_completion(&ddata->ack); ddata->dev = &pdev->dev; platform_set_drvdata(pdev, ddata); /* Active state selected in phy_mdm6600_power_on() */ error = pinctrl_pm_select_sleep_state(ddata->dev); if (error) dev_warn(ddata->dev, "%s: error with sleep_state: %i\n", __func__, error); error = phy_mdm6600_init_lines(ddata); if (error) return error; phy_mdm6600_init_irq(ddata); schedule_delayed_work(&ddata->bootup_work, 0); /* * See phy_mdm6600_device_power_on(). We should be able * to remove this eventually when ohci-platform can deal * with -EPROBE_DEFER. */ msleep(PHY_MDM6600_PHY_DELAY_MS + 500); /* * Enable PM runtime only after PHY has been powered up properly. * It is currently only needed after USB suspends mdm6600 and n_gsm * needs to access the device. We don't want to do this earlier as * gpio mode0 pin doubles as mdm6600 wake-up gpio. */ pm_runtime_use_autosuspend(ddata->dev); pm_runtime_set_autosuspend_delay(ddata->dev, MDM6600_MODEM_IDLE_DELAY_MS); pm_runtime_enable(ddata->dev); error = pm_runtime_get_sync(ddata->dev); if (error < 0) { dev_warn(ddata->dev, "failed to wake modem: %i\n", error); pm_runtime_put_noidle(ddata->dev); goto cleanup; } ddata->generic_phy = devm_phy_create(ddata->dev, NULL, &gpio_usb_ops); if (IS_ERR(ddata->generic_phy)) { error = PTR_ERR(ddata->generic_phy); goto idle; } phy_set_drvdata(ddata->generic_phy, ddata); ddata->phy_provider = devm_of_phy_provider_register(ddata->dev, of_phy_simple_xlate); if (IS_ERR(ddata->phy_provider)) error = PTR_ERR(ddata->phy_provider); idle: pm_runtime_mark_last_busy(ddata->dev); pm_runtime_put_autosuspend(ddata->dev); cleanup: if (error < 0) phy_mdm6600_device_power_off(ddata); pm_runtime_disable(ddata->dev); pm_runtime_dont_use_autosuspend(ddata->dev); return error; } static int phy_mdm6600_remove(struct platform_device *pdev) { struct phy_mdm6600 *ddata = platform_get_drvdata(pdev); struct gpio_desc *reset_gpio = ddata->ctrl_gpios[PHY_MDM6600_RESET]; pm_runtime_dont_use_autosuspend(ddata->dev); pm_runtime_put_sync(ddata->dev); pm_runtime_disable(ddata->dev); if (!ddata->running) wait_for_completion_timeout(&ddata->ack, msecs_to_jiffies(PHY_MDM6600_ENABLED_DELAY_MS)); gpiod_set_value_cansleep(reset_gpio, 1); phy_mdm6600_device_power_off(ddata); cancel_delayed_work_sync(&ddata->modem_wake_work); cancel_delayed_work_sync(&ddata->bootup_work); cancel_delayed_work_sync(&ddata->status_work); return 0; } static struct platform_driver phy_mdm6600_driver = { .probe = phy_mdm6600_probe, .remove = phy_mdm6600_remove, .driver = { .name = "phy-mapphone-mdm6600", .pm = &phy_mdm6600_pm_ops, .of_match_table = of_match_ptr(phy_mdm6600_id_table), }, }; module_platform_driver(phy_mdm6600_driver); MODULE_ALIAS("platform:gpio_usb"); MODULE_AUTHOR("Tony Lindgren "); MODULE_DESCRIPTION("mdm6600 gpio usb phy driver"); MODULE_LICENSE("GPL v2");