Commit 12e725e0 authored by Martin Blumenstingl's avatar Martin Blumenstingl
Browse files

usb: common: Add an EXTCON based USB connection detection driver - WiP

parent c6af84dd
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -36,6 +36,18 @@ config USB_ULPI_BUS
	  To compile this driver as a module, choose M here: the module will
	  be called ulpi.

config USB_CONN_EXTCON
	tristate "USB EXTCON Based Connection Detection Driver"
	depends on EXTCON
	select USB_ROLE_SWITCH
	help
	  The driver supports USB role switch between host and device via
	  extcon based USB cable detection, used for example if the USB PHY has
	  built-in cable state detection.

	  To compile the driver as a module, choose M here: the module will
	  be called usb-conn-extcon.ko

config USB_CONN_GPIO
	tristate "USB GPIO Based Connection Detection Driver"
	depends on GPIOLIB
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ usb-common-y += common.o
usb-common-$(CONFIG_TRACING)	  += debug.o
usb-common-$(CONFIG_USB_LED_TRIG) += led.o

obj-$(CONFIG_USB_CONN_EXTCON)	+= usb-conn-extcon.o
obj-$(CONFIG_USB_CONN_GPIO)	+= usb-conn-gpio.o
obj-$(CONFIG_USB_OTG_FSM) += usb-otg-fsm.o
obj-$(CONFIG_USB_ULPI_BUS)	+= ulpi.o
+180 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0
/*
 * USB EXTCON Based Connection Detection Driver
 *
 * Copyright (C) 2020 Martin Blumenstingl <martin.blumenstingl@googlemail.com>
 *
 * Based on USB GPIO Based Connection Detection Driver:
 *   Copyright (C) 2019 MediaTek Inc.
 *   Author: Chunfeng Yun <chunfeng.yun@mediatek.com>
 */

#include <linux/device.h>
#include <linux/extcon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regulator/consumer.h>
#include <linux/usb/role.h>

#define EXTCON_USB_CONN_DEBOUNCE_MS				20

struct extcon_usb_conn_info {
	struct device *dev;
	struct usb_role_switch *role_sw;
	enum usb_role last_role;
	struct regulator *vbus;
	struct delayed_work dw_det;

	struct extcon_dev *edev;
	struct notifier_block edev_nb;
};

static void extcon_usb_conn_detect_cable(struct work_struct *work)
{
	struct extcon_usb_conn_info *info;
	enum usb_role new_role;
	int ret;

	info = container_of(to_delayed_work(work),
			    struct extcon_usb_conn_info, dw_det);

	if (extcon_get_state(info->edev, EXTCON_USB_HOST) > 0)
		new_role = USB_ROLE_HOST;
	else if (extcon_get_state(info->edev, EXTCON_USB) > 0)
		new_role = USB_ROLE_DEVICE;
	else
		new_role = USB_ROLE_NONE;

	if (info->last_role == new_role) {
		dev_dbg(info->dev, "Repeated role: %u\n", new_role);
		return;
	}

	dev_dbg(info->dev, "Switching from role %u to %u\n", info->last_role,
		new_role);

	if (info->last_role == USB_ROLE_HOST)
		regulator_disable(info->vbus);

	ret = usb_role_switch_set_role(info->role_sw, new_role);
	if (ret)
		dev_err(info->dev, "Failed to set role: %d\n", ret);

	if (new_role == USB_ROLE_HOST) {
		ret = regulator_enable(info->vbus);
		if (ret)
			dev_err(info->dev,
				"Failed to enable the VBUS regulator\n");
	}

	info->last_role = new_role;

	dev_dbg(info->dev, "VBUS regulator is %s\n",
		regulator_is_enabled(info->vbus) ? "enabled" : "disabled");
}

static void extcon_usb_conn_queue_work(struct extcon_usb_conn_info *info,
				       unsigned long delay_jiffies)
{
	queue_delayed_work(system_power_efficient_wq, &info->dw_det,
			   delay_jiffies);
}

static int extcon_usb_conn_edev_notifier(struct notifier_block *nb,
					 unsigned long event, void *ptr)
{
	struct extcon_usb_conn_info *info;

	info = container_of(nb, struct extcon_usb_conn_info, edev_nb);

	extcon_usb_conn_queue_work(info,
				   msecs_to_jiffies(EXTCON_USB_CONN_DEBOUNCE_MS));

	return NOTIFY_DONE;
}

static int extcon_usb_conn_probe(struct platform_device *pdev)
{
	struct extcon_usb_conn_info *info;
	struct device *dev = &pdev->dev;
	int ret;

	info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL);
	if (!info)
		return -ENOMEM;

	info->dev = dev;
	info->last_role = USB_ROLE_NONE;
	info->edev = extcon_get_edev_by_phandle(dev, 0);
	if (IS_ERR(info->edev))
		return PTR_ERR(info->edev);

	info->vbus = devm_regulator_get(dev, "vbus");
	if (IS_ERR(info->vbus)) {
		if (PTR_ERR(info->vbus) != -EPROBE_DEFER)
			dev_err(dev, "Failed to get VBUS regulator\n");
		return PTR_ERR(info->vbus);
	}

	info->role_sw = usb_role_switch_get(dev);
	if (IS_ERR(info->role_sw)) {
		if (PTR_ERR(info->role_sw) != -EPROBE_DEFER)
			dev_err(dev, "Failed to get USB role switch\n");

		return PTR_ERR(info->role_sw);
	}

	INIT_DELAYED_WORK(&info->dw_det, extcon_usb_conn_detect_cable);

	info->edev_nb.notifier_call = extcon_usb_conn_edev_notifier;
	ret = extcon_register_notifier_all(info->edev, &info->edev_nb);
	if (ret)
		goto err_put_role_sw;

	platform_set_drvdata(pdev, info);

	/* Update the role based on cable state at startup */
	extcon_usb_conn_queue_work(info, 0);

	return 0;

err_put_role_sw:
	usb_role_switch_put(info->role_sw);
	return ret;
}

static int extcon_usb_conn_remove(struct platform_device *pdev)
{
	struct extcon_usb_conn_info *info = platform_get_drvdata(pdev);

	cancel_delayed_work_sync(&info->dw_det);

	usb_role_switch_put(info->role_sw);

	if (info->last_role == USB_ROLE_HOST)
		regulator_disable(info->vbus);

	return 0;
}

static const struct of_device_id extcon_usb_conn_of_match[] = {
	{ .compatible = "extcon-usb-b-connector", },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, extcon_usb_conn_dt_match);

static struct platform_driver extcon_usb_conn_driver = {
	.probe		= extcon_usb_conn_probe,
	.remove		= extcon_usb_conn_remove,
	.driver		= {
		.name	= "extcon-usb-conn-gpio",
		.of_match_table = extcon_usb_conn_of_match,
	},
};

module_platform_driver(extcon_usb_conn_driver);

MODULE_AUTHOR("Martin Blumenstingl <martin.blumenstingl@googlemail.com>");
MODULE_DESCRIPTION("USB EXTCON based connection detection driver");
MODULE_LICENSE("GPL v2");