From dc7c80a83c9aa6dd7792390462015bd499fd4046 Mon Sep 17 00:00:00 2001
From: Andrey Smirnov <andrew.smirnov@gmail.com>
Date: Sun, 27 Feb 2022 14:46:08 -0800
Subject: [PATCH 07/21] extcon: Add driver for Steam Deck

(cherry picked from commit f9f2eddae582ae39d5f89c1218448fc259b90aa8)
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
---
 drivers/extcon/Kconfig            |   7 ++
 drivers/extcon/Makefile           |   1 +
 drivers/extcon/extcon-steamdeck.c | 180 ++++++++++++++++++++++++++++++
 3 files changed, 188 insertions(+)
 create mode 100644 drivers/extcon/extcon-steamdeck.c

diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig
index 3da94b382292..88ac6e28689c 100644
--- a/drivers/extcon/Kconfig
+++ b/drivers/extcon/Kconfig
@@ -203,4 +203,11 @@ config EXTCON_RTK_TYPE_C
 	  The DHC (Digital Home Hub) RTD series SoC contains a type c module.
 	  This driver will detect the status of the type-c port.
 
+config EXTCON_STEAMDECK
+	tristate "Steam Deck extcon support"
+	depends on MFD_STEAMDECK
+	help
+	  Say Y here to enable support of USB Type C cable detection extcon
+	  support on Steam Deck devices
+
 endif
diff --git a/drivers/extcon/Makefile b/drivers/extcon/Makefile
index f779adb5e4c7..c7f3c908b329 100644
--- a/drivers/extcon/Makefile
+++ b/drivers/extcon/Makefile
@@ -25,4 +25,5 @@ obj-$(CONFIG_EXTCON_SM5502)	+= extcon-sm5502.o
 obj-$(CONFIG_EXTCON_USB_GPIO)	+= extcon-usb-gpio.o
 obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o
 obj-$(CONFIG_EXTCON_USBC_TUSB320) += extcon-usbc-tusb320.o
+obj-$(CONFIG_EXTCON_STEAMDECK)  += extcon-steamdeck.o
 obj-$(CONFIG_EXTCON_RTK_TYPE_C) += extcon-rtk-type-c.o
diff --git a/drivers/extcon/extcon-steamdeck.c b/drivers/extcon/extcon-steamdeck.c
new file mode 100644
index 000000000000..74f190adc8ea
--- /dev/null
+++ b/drivers/extcon/extcon-steamdeck.c
@@ -0,0 +1,180 @@
+
+#include <linux/acpi.h>
+#include <linux/platform_device.h>
+#include <linux/extcon-provider.h>
+
+#define ACPI_STEAMDECK_NOTIFY_STATUS	0x80
+
+/* 0 - port connected, 1 -port disconnected */
+#define ACPI_STEAMDECK_PORT_CONNECT	BIT(0)
+/* 0 - Upstream Facing Port, 1 - Downdstream Facing Port */
+#define ACPI_STEAMDECK_CUR_DATA_ROLE	BIT(3)
+/*
+ * Debouncing delay to allow negotiation process to settle. 2s value
+ * was arrived at via trial and error.
+ */
+#define STEAMDECK_ROLE_SWITCH_DELAY	(msecs_to_jiffies(2000))
+
+struct steamdeck_extcon {
+	struct acpi_device *adev;
+	struct delayed_work role_work;
+	struct extcon_dev *edev;
+	struct device *dev;
+};
+
+static int steamdeck_read_pdcs(struct steamdeck_extcon *sd, unsigned long long *pdcs)
+{
+	acpi_status status;
+
+	status = acpi_evaluate_integer(sd->adev->handle, "PDCS", NULL, pdcs);
+	if (ACPI_FAILURE(status)) {
+		dev_err(sd->dev, "PDCS evaluation failed: %s\n",
+			acpi_format_exception(status));
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static void steamdeck_usb_role_work(struct work_struct *work)
+{
+	struct steamdeck_extcon *sd =
+		container_of(work, struct steamdeck_extcon, role_work.work);
+	unsigned long long pdcs;
+	bool usb_host;
+
+	if (steamdeck_read_pdcs(sd, &pdcs))
+		return;
+
+	/*
+	 * We only care about these two
+	 */
+	pdcs &= ACPI_STEAMDECK_PORT_CONNECT | ACPI_STEAMDECK_CUR_DATA_ROLE;
+
+	/*
+	 * For "connect" events our role is determined by a bit in
+	 * PDCS, for "disconnect" we switch to being a gadget
+	 * unconditionally. The thinking for the latter is we don't
+	 * want to start acting as a USB host until we get
+	 * confirmation from the firmware that we are a USB host
+	 */
+	usb_host = (pdcs & ACPI_STEAMDECK_PORT_CONNECT) ?
+		pdcs & ACPI_STEAMDECK_CUR_DATA_ROLE : false;
+
+	dev_dbg(sd->dev, "USB role is %s\n", usb_host ? "host" : "device");
+	WARN_ON(extcon_set_state_sync(sd->edev, EXTCON_USB_HOST,
+				      usb_host));
+
+}
+
+static void steamdeck_notify(acpi_handle handle, u32 event, void *context)
+{
+	struct device *dev = context;
+	struct steamdeck_extcon *sd = dev_get_drvdata(dev);
+	unsigned long long pdcs;
+	unsigned long delay;
+
+	switch (event) {
+	case ACPI_STEAMDECK_NOTIFY_STATUS:
+		if (steamdeck_read_pdcs(sd, &pdcs))
+			return;
+		/*
+		 * We process "disconnect" events immediately and
+		 * "connect" events with a delay to give the HW time
+		 * to settle. For example attaching USB hub (at least
+		 * for HW used for testing) will generate intermediary
+		 * event with "host" bit not set, followed by the one
+		 * that does have it set.
+		 */
+		delay = (pdcs & ACPI_STEAMDECK_PORT_CONNECT) ?
+			STEAMDECK_ROLE_SWITCH_DELAY : 0;
+
+		queue_delayed_work(system_long_wq, &sd->role_work, delay);
+		break;
+	default:
+		dev_warn(dev, "Unsupported event [0x%x]\n", event);
+	}
+}
+
+static void steamdeck_remove_notify_handler(void *data)
+{
+	struct steamdeck_extcon *sd = data;
+
+	acpi_remove_notify_handler(sd->adev->handle, ACPI_DEVICE_NOTIFY,
+				   steamdeck_notify);
+	cancel_delayed_work_sync(&sd->role_work);
+}
+
+static const unsigned int steamdeck_extcon_cable[] = {
+	EXTCON_USB,
+	EXTCON_USB_HOST,
+	EXTCON_CHG_USB_SDP,
+	EXTCON_CHG_USB_CDP,
+	EXTCON_CHG_USB_DCP,
+	EXTCON_CHG_USB_ACA,
+	EXTCON_NONE,
+};
+
+static int steamdeck_extcon_probe(struct platform_device *pdev)
+{
+  	struct device *dev = &pdev->dev;
+	struct steamdeck_extcon *sd;
+	acpi_status status;
+	int ret;
+
+	sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+
+	INIT_DELAYED_WORK(&sd->role_work, steamdeck_usb_role_work);
+	platform_set_drvdata(pdev, sd);
+	sd->adev = ACPI_COMPANION(dev->parent);
+	sd->dev  = dev;
+	sd->edev = devm_extcon_dev_allocate(dev, steamdeck_extcon_cable);
+	if (IS_ERR(sd->edev))
+		return PTR_ERR(sd->edev);
+
+	ret = devm_extcon_dev_register(dev, sd->edev);
+	if (ret < 0) {
+		dev_err(dev, "Failed to register extcon device: %d\n", ret);
+		return ret;
+	}
+
+	/*
+	 * Set initial role value
+	 */
+	queue_delayed_work(system_long_wq, &sd->role_work, 0);
+	flush_delayed_work(&sd->role_work);
+
+	status = acpi_install_notify_handler(sd->adev->handle,
+					     ACPI_DEVICE_NOTIFY,
+					     steamdeck_notify,
+					     dev);
+	if (ACPI_FAILURE(status)) {
+		dev_err(dev, "Error installing ACPI notify handler\n");
+		return -EIO;
+	}
+
+	ret = devm_add_action_or_reset(dev, steamdeck_remove_notify_handler,
+				       sd);
+	return ret;
+}
+
+static const struct platform_device_id steamdeck_extcon_id_table[] = {
+	{ .name = "steamdeck-extcon" },
+	{}
+};
+MODULE_DEVICE_TABLE(platform, steamdeck_extcon_id_table);
+
+static struct platform_driver steamdeck_extcon_driver = {
+	.probe = steamdeck_extcon_probe,
+	.driver = {
+		.name = "steamdeck-extcon",
+	},
+	.id_table = steamdeck_extcon_id_table,
+};
+module_platform_driver(steamdeck_extcon_driver);
+
+MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
+MODULE_DESCRIPTION("Steam Deck extcon driver");
+MODULE_LICENSE("GPL");
-- 
2.45.2

