aboutsummaryrefslogtreecommitdiff
path: root/drivers/platform
diff options
context:
space:
mode:
authorGravatar Hans de Goede <hdegoede@redhat.com> 2024-04-22 15:16:49 +0200
committerGravatar Hans de Goede <hdegoede@redhat.com> 2024-04-29 12:46:43 +0200
commitb2ed33e8d486ab2f1920131dd76fab38c8ef3550 (patch)
tree51e886809a8b8493cfa99f5d9ec16f5fece3c7e3 /drivers/platform
parentplatform/x86: wmi: Add MSI WMI Platform driver (diff)
downloadlinux-b2ed33e8d486ab2f1920131dd76fab38c8ef3550.tar.gz
linux-b2ed33e8d486ab2f1920131dd76fab38c8ef3550.tar.bz2
linux-b2ed33e8d486ab2f1920131dd76fab38c8ef3550.zip
platform/x86: Add lenovo-yoga-tab2-pro-1380-fastcharger driver
Add a new driver for the custom fast charging protocol found on Lenovo Yoga Tablet 2 1380F / 1380L models. Signed-off-by: Hans de Goede <hdegoede@redhat.com> Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com> Link: https://lore.kernel.org/r/20240422131649.46002-1-hdegoede@redhat.com
Diffstat (limited to 'drivers/platform')
-rw-r--r--drivers/platform/x86/Kconfig11
-rw-r--r--drivers/platform/x86/Makefile1
-rw-r--r--drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c338
3 files changed, 350 insertions, 0 deletions
diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index b744f18bbfa7..e5601d8fba68 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -133,6 +133,17 @@ config YOGABOOK
To compile this driver as a module, choose M here: the module will
be called lenovo-yogabook.
+config YT2_1380
+ tristate "Lenovo Yoga Tablet 2 1380 fast charge driver"
+ depends on SERIAL_DEV_BUS
+ depends on ACPI
+ help
+ Say Y here to enable support for the custom fast charging protocol
+ found on the Lenovo Yoga Tablet 2 1380F / 1380L models.
+
+ To compile this driver as a module, choose M here: the module will
+ be called lenovo-yogabook.
+
config ACERHDF
tristate "Acer Aspire One temperature and fan driver"
depends on ACPI && THERMAL
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 26b8af55738b..cf3032e4e27f 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -66,6 +66,7 @@ obj-$(CONFIG_SENSORS_HDAPS) += hdaps.o
obj-$(CONFIG_THINKPAD_ACPI) += thinkpad_acpi.o
obj-$(CONFIG_THINKPAD_LMI) += think-lmi.o
obj-$(CONFIG_YOGABOOK) += lenovo-yogabook.o
+obj-$(CONFIG_YT2_1380) += lenovo-yoga-tab2-pro-1380-fastcharger.o
obj-$(CONFIG_LENOVO_WMI_CAMERA) += lenovo-wmi-camera.o
# Intel
diff --git a/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c b/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c
new file mode 100644
index 000000000000..d525bdc8ca9b
--- /dev/null
+++ b/drivers/platform/x86/lenovo-yoga-tab2-pro-1380-fastcharger.c
@@ -0,0 +1,338 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Support for the custom fast charging protocol found on the Lenovo Yoga
+ * Tablet 2 1380F / 1380L models.
+ *
+ * Copyright (C) 2024 Hans de Goede <hansg@kernel.org>
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/extcon.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/notifier.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/pinctrl/machine.h>
+#include <linux/platform_device.h>
+#include <linux/serdev.h>
+#include <linux/time.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+#include "serdev_helpers.h"
+
+#define YT2_1380_FC_PDEV_NAME "lenovo-yoga-tab2-pro-1380-fastcharger"
+#define YT2_1380_FC_SERDEV_CTRL "serial0"
+#define YT2_1380_FC_SERDEV_NAME "serial0-0"
+#define YT2_1380_FC_EXTCON_NAME "i2c-lc824206xa"
+
+#define YT2_1380_FC_MAX_TRIES 5
+#define YT2_1380_FC_PIN_SW_DELAY_US (10 * USEC_PER_MSEC)
+#define YT2_1380_FC_UART_DRAIN_DELAY_US (50 * USEC_PER_MSEC)
+#define YT2_1380_FC_VOLT_SW_DELAY_US (1000 * USEC_PER_MSEC)
+
+struct yt2_1380_fc {
+ struct device *dev;
+ struct pinctrl *pinctrl;
+ struct pinctrl_state *gpio_state;
+ struct pinctrl_state *uart_state;
+ struct gpio_desc *uart3_txd;
+ struct gpio_desc *uart3_rxd;
+ struct extcon_dev *extcon;
+ struct notifier_block nb;
+ struct work_struct work;
+ bool fast_charging;
+};
+
+static int yt2_1380_fc_set_gpio_mode(struct yt2_1380_fc *fc, bool enable)
+{
+ struct pinctrl_state *state = enable ? fc->gpio_state : fc->uart_state;
+ int ret;
+
+ ret = pinctrl_select_state(fc->pinctrl, state);
+ if (ret) {
+ dev_err(fc->dev, "Error %d setting pinctrl state\n", ret);
+ return ret;
+ }
+
+ fsleep(YT2_1380_FC_PIN_SW_DELAY_US);
+ return 0;
+}
+
+static bool yt2_1380_fc_dedicated_charger_connected(struct yt2_1380_fc *fc)
+{
+ return extcon_get_state(fc->extcon, EXTCON_CHG_USB_DCP) > 0;
+}
+
+static bool yt2_1380_fc_fast_charger_connected(struct yt2_1380_fc *fc)
+{
+ return extcon_get_state(fc->extcon, EXTCON_CHG_USB_FAST) > 0;
+}
+
+static void yt2_1380_fc_worker(struct work_struct *work)
+{
+ struct yt2_1380_fc *fc = container_of(work, struct yt2_1380_fc, work);
+ int i, ret;
+
+ /* Do nothing if already fast charging */
+ if (yt2_1380_fc_fast_charger_connected(fc))
+ return;
+
+ for (i = 0; i < YT2_1380_FC_MAX_TRIES; i++) {
+ /* Set pins to UART mode (for charger disconnect and retries) */
+ ret = yt2_1380_fc_set_gpio_mode(fc, false);
+ if (ret)
+ return;
+
+ /* Only try 12V charging if a dedicated charger is detected */
+ if (!yt2_1380_fc_dedicated_charger_connected(fc))
+ return;
+
+ /* Send the command to switch to 12V charging */
+ ret = serdev_device_write_buf(to_serdev_device(fc->dev), "SC", strlen("SC"));
+ if (ret != strlen("SC")) {
+ dev_err(fc->dev, "Error %d writing to uart\n", ret);
+ return;
+ }
+
+ fsleep(YT2_1380_FC_UART_DRAIN_DELAY_US);
+
+ /* Re-check a charger is still connected */
+ if (!yt2_1380_fc_dedicated_charger_connected(fc))
+ return;
+
+ /*
+ * Now switch the lines to GPIO (output, high). The charger
+ * expects the lines being driven high after the command.
+ * Presumably this is used to detect the tablet getting
+ * unplugged (to switch back to 5V output on unplug).
+ */
+ ret = yt2_1380_fc_set_gpio_mode(fc, true);
+ if (ret)
+ return;
+
+ fsleep(YT2_1380_FC_VOLT_SW_DELAY_US);
+
+ if (yt2_1380_fc_fast_charger_connected(fc))
+ return; /* Success */
+ }
+
+ dev_dbg(fc->dev, "Failed to switch to 12V charging (not the original charger?)\n");
+ /* Failed to enable 12V fast charging, reset pins to default UART mode */
+ yt2_1380_fc_set_gpio_mode(fc, false);
+}
+
+static int yt2_1380_fc_extcon_evt(struct notifier_block *nb,
+ unsigned long event, void *param)
+{
+ struct yt2_1380_fc *fc = container_of(nb, struct yt2_1380_fc, nb);
+
+ schedule_work(&fc->work);
+ return NOTIFY_OK;
+}
+
+static size_t yt2_1380_fc_receive(struct serdev_device *serdev, const u8 *data, size_t len)
+{
+ /*
+ * Since the USB data lines are shorted for DCP detection, echos of
+ * the "SC" command send in yt2_1380_fc_worker() will be received.
+ */
+ dev_dbg(&serdev->dev, "recv: %*ph\n", (int)len, data);
+ return len;
+}
+
+static const struct serdev_device_ops yt2_1380_fc_serdev_ops = {
+ .receive_buf = yt2_1380_fc_receive,
+ .write_wakeup = serdev_device_write_wakeup,
+};
+
+static int yt2_1380_fc_serdev_probe(struct serdev_device *serdev)
+{
+ struct device *dev = &serdev->dev;
+ struct yt2_1380_fc *fc;
+ int ret;
+
+ fc = devm_kzalloc(dev, sizeof(*fc), GFP_KERNEL);
+ if (!fc)
+ return -ENOMEM;
+
+ fc->dev = dev;
+ fc->nb.notifier_call = yt2_1380_fc_extcon_evt;
+ INIT_WORK(&fc->work, yt2_1380_fc_worker);
+
+ /*
+ * Do this first since it may return -EPROBE_DEFER.
+ * There is no extcon_put(), so there is no need to free this.
+ */
+ fc->extcon = extcon_get_extcon_dev(YT2_1380_FC_EXTCON_NAME);
+ if (IS_ERR(fc->extcon))
+ return dev_err_probe(dev, PTR_ERR(fc->extcon), "getting extcon\n");
+
+ fc->pinctrl = devm_pinctrl_get(dev);
+ if (IS_ERR(fc->pinctrl))
+ return dev_err_probe(dev, PTR_ERR(fc->pinctrl), "getting pinctrl\n");
+
+ /*
+ * To switch the UART3 pins connected to the USB data lines between
+ * UART and GPIO modes.
+ */
+ fc->gpio_state = pinctrl_lookup_state(fc->pinctrl, "uart3_gpio");
+ fc->uart_state = pinctrl_lookup_state(fc->pinctrl, "uart3_uart");
+ if (IS_ERR(fc->gpio_state) || IS_ERR(fc->uart_state))
+ return dev_err_probe(dev, -EINVAL, "getting pinctrl states\n");
+
+ ret = yt2_1380_fc_set_gpio_mode(fc, true);
+ if (ret)
+ return ret;
+
+ fc->uart3_txd = devm_gpiod_get(dev, "uart3_txd", GPIOD_OUT_HIGH);
+ if (IS_ERR(fc->uart3_txd))
+ return dev_err_probe(dev, PTR_ERR(fc->uart3_txd), "getting uart3_txd gpio\n");
+
+ fc->uart3_rxd = devm_gpiod_get(dev, "uart3_rxd", GPIOD_OUT_HIGH);
+ if (IS_ERR(fc->uart3_rxd))
+ return dev_err_probe(dev, PTR_ERR(fc->uart3_rxd), "getting uart3_rxd gpio\n");
+
+ ret = yt2_1380_fc_set_gpio_mode(fc, false);
+ if (ret)
+ return ret;
+
+ ret = devm_serdev_device_open(dev, serdev);
+ if (ret)
+ return dev_err_probe(dev, ret, "opening UART device\n");
+
+ serdev_device_set_baudrate(serdev, 600);
+ serdev_device_set_flow_control(serdev, false);
+ serdev_device_set_drvdata(serdev, fc);
+ serdev_device_set_client_ops(serdev, &yt2_1380_fc_serdev_ops);
+
+ ret = devm_extcon_register_notifier_all(dev, fc->extcon, &fc->nb);
+ if (ret)
+ return dev_err_probe(dev, ret, "registering extcon notifier\n");
+
+ /* In case the extcon already has detected a DCP charger */
+ schedule_work(&fc->work);
+
+ return 0;
+}
+
+struct serdev_device_driver yt2_1380_fc_serdev_driver = {
+ .probe = yt2_1380_fc_serdev_probe,
+ .driver = {
+ .name = KBUILD_MODNAME,
+ },
+};
+
+static const struct pinctrl_map yt2_1380_fc_pinctrl_map[] = {
+ PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_uart",
+ "INT33FC:00", "uart3_grp", "uart"),
+ PIN_MAP_MUX_GROUP(YT2_1380_FC_SERDEV_NAME, "uart3_gpio",
+ "INT33FC:00", "uart3_grp_gpio", "gpio"),
+};
+
+static int yt2_1380_fc_pdev_probe(struct platform_device *pdev)
+{
+ struct serdev_device *serdev;
+ struct device *ctrl_dev;
+ int ret;
+
+ /* Register pinctrl mappings for setting the UART3 pins mode */
+ ret = pinctrl_register_mappings(yt2_1380_fc_pinctrl_map,
+ ARRAY_SIZE(yt2_1380_fc_pinctrl_map));
+ if (ret)
+ return ret;
+
+ /* And create the serdev to talk to the charger over the UART3 pins */
+ ctrl_dev = get_serdev_controller("PNP0501", "1", 0, YT2_1380_FC_SERDEV_CTRL);
+ if (IS_ERR(ctrl_dev)) {
+ ret = PTR_ERR(ctrl_dev);
+ goto out_pinctrl_unregister_mappings;
+ }
+
+ serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev));
+ put_device(ctrl_dev);
+ if (!serdev) {
+ ret = -ENOMEM;
+ goto out_pinctrl_unregister_mappings;
+ }
+
+ ret = serdev_device_add(serdev);
+ if (ret) {
+ dev_err_probe(&pdev->dev, ret, "adding serdev\n");
+ serdev_device_put(serdev);
+ goto out_pinctrl_unregister_mappings;
+ }
+
+ /*
+ * serdev device <-> driver matching relies on OF or ACPI matches and
+ * neither is available here, manually bind the driver.
+ */
+ ret = device_driver_attach(&yt2_1380_fc_serdev_driver.driver, &serdev->dev);
+ if (ret) {
+ /* device_driver_attach() maps EPROBE_DEFER to EAGAIN, map it back */
+ ret = (ret == -EAGAIN) ? -EPROBE_DEFER : ret;
+ dev_err_probe(&pdev->dev, ret, "attaching serdev driver\n");
+ goto out_serdev_device_remove;
+ }
+
+ /* So that yt2_1380_fc_pdev_remove() can remove the serdev */
+ platform_set_drvdata(pdev, serdev);
+ return 0;
+
+out_serdev_device_remove:
+ serdev_device_remove(serdev);
+out_pinctrl_unregister_mappings:
+ pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map);
+ return ret;
+}
+
+static void yt2_1380_fc_pdev_remove(struct platform_device *pdev)
+{
+ struct serdev_device *serdev = platform_get_drvdata(pdev);
+
+ serdev_device_remove(serdev);
+ pinctrl_unregister_mappings(yt2_1380_fc_pinctrl_map);
+}
+
+static struct platform_driver yt2_1380_fc_pdev_driver = {
+ .probe = yt2_1380_fc_pdev_probe,
+ .remove_new = yt2_1380_fc_pdev_remove,
+ .driver = {
+ .name = YT2_1380_FC_PDEV_NAME,
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+};
+
+static int __init yt2_1380_fc_module_init(void)
+{
+ int ret;
+
+ /*
+ * serdev driver MUST be registered first because pdev driver calls
+ * device_driver_attach() on the serdev, serdev-driver pair.
+ */
+ ret = serdev_device_driver_register(&yt2_1380_fc_serdev_driver);
+ if (ret)
+ return ret;
+
+ ret = platform_driver_register(&yt2_1380_fc_pdev_driver);
+ if (ret)
+ serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver);
+
+ return ret;
+}
+module_init(yt2_1380_fc_module_init);
+
+static void __exit yt2_1380_fc_module_exit(void)
+{
+ platform_driver_unregister(&yt2_1380_fc_pdev_driver);
+ serdev_device_driver_unregister(&yt2_1380_fc_serdev_driver);
+}
+module_exit(yt2_1380_fc_module_exit);
+
+MODULE_ALIAS("platform:" YT2_1380_FC_PDEV_NAME);
+MODULE_DESCRIPTION("Lenovo Yoga Tablet 2 1380 fast charge driver");
+MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
+MODULE_LICENSE("GPL");