root/drivers/platform/x86/lenovo/yoga-tab2-pro-1380-fastcharger.c
// 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;

        serdev_device_set_drvdata(serdev, fc);
        serdev_device_set_client_ops(serdev, &yt2_1380_fc_serdev_ops);

        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);

        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;
}

static 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 = devm_pinctrl_register_mappings(&pdev->dev, 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))
                return PTR_ERR(ctrl_dev);

        serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev));
        put_device(ctrl_dev);
        if (!serdev)
                return -ENOMEM;

        /* Propagate pdev-fwnode set by x86-android-tablets to serdev */
        device_set_node(&serdev->dev, dev_fwnode(&pdev->dev));
        /* The fwnode is a managed node, so it will be auto-put on serdev_device_put() */
        fwnode_handle_get(dev_fwnode(&serdev->dev));

        ret = serdev_device_add(serdev);
        if (ret) {
                serdev_device_put(serdev);
                return dev_err_probe(&pdev->dev, ret, "adding serdev\n");
        }

        /*
         * 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 */
                serdev_device_remove(serdev);
                return dev_err_probe(&pdev->dev,
                                     (ret == -EAGAIN) ? -EPROBE_DEFER : ret,
                                     "attaching serdev driver\n");
        }

        /* So that yt2_1380_fc_pdev_remove() can remove the serdev */
        platform_set_drvdata(pdev, serdev);
        return 0;
}

static void yt2_1380_fc_pdev_remove(struct platform_device *pdev)
{
        struct serdev_device *serdev = platform_get_drvdata(pdev);

        serdev_device_remove(serdev);
}

static struct platform_driver yt2_1380_fc_pdev_driver = {
        .probe = yt2_1380_fc_pdev_probe,
        .remove = 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");