summary refs log tree commit diff
diff options
context:
space:
mode:
authorCristian Ciocaltea <cristian.ciocaltea@collabora.com>2023-09-15 19:15:49 +0300
committerCristian Ciocaltea <cristian.ciocaltea@collabora.com>2023-09-15 19:15:49 +0300
commitcdd69777f59ed04bff40e11728ebf844d0b820e9 (patch)
tree064b82035a8f233ca0ff48494e6463c036891425
parent2feb87f7fea61171eb84ccb9c54c01bac2714326 (diff)
parent6a4d2bea1e5b5aa150e11a40b2ac4098de3c6491 (diff)
downloadlinux-cdd69777f59ed04bff40e11728ebf844d0b820e9.tar.gz
Merge branch 6.1/features/steamdeck-mfd
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
-rw-r--r--drivers/extcon/Kconfig7
-rw-r--r--drivers/extcon/Makefile1
-rw-r--r--drivers/extcon/extcon-steamdeck.c180
-rw-r--r--drivers/hwmon/Kconfig11
-rw-r--r--drivers/hwmon/Makefile1
-rw-r--r--drivers/hwmon/steamdeck-hwmon.c294
-rw-r--r--drivers/leds/Kconfig7
-rw-r--r--drivers/leds/Makefile1
-rw-r--r--drivers/leds/leds-steamdeck.c74
-rw-r--r--drivers/mfd/Kconfig11
-rw-r--r--drivers/mfd/Makefile2
-rw-r--r--drivers/mfd/steamdeck.c127
12 files changed, 716 insertions, 0 deletions
diff --git a/drivers/extcon/Kconfig b/drivers/extcon/Kconfig
index 290186e44e6b..4d444a9e2c1f 100644
--- a/drivers/extcon/Kconfig
+++ b/drivers/extcon/Kconfig
@@ -189,4 +189,11 @@ config EXTCON_USBC_TUSB320
 	  Say Y here to enable support for USB Type C cable detection extcon
 	  support using a TUSB320.
 
+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 1b390d934ca9..1c7e217f29e4 100644
--- a/drivers/extcon/Makefile
+++ b/drivers/extcon/Makefile
@@ -25,3 +25,4 @@ 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
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");
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index a5143d01b95f..92e096785eec 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1901,6 +1901,17 @@ config SENSORS_SCH5636
 	  This driver can also be built as a module. If so, the module
 	  will be called sch5636.
 
+config SENSORS_STEAMDECK
+	tristate "Steam Deck EC sensors"
+	depends on MFD_STEAMDECK
+	help
+	  If you say yes here you get support for the hardware
+	  monitoring features exposed by EC firmware on Steam Deck
+	  devices
+
+	  This driver can also be built as a module. If so, the module
+	  will be called steamdeck-hwmon.
+
 config SENSORS_STTS751
 	tristate "ST Microelectronics STTS751"
 	depends on I2C
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 11d076cad8a2..d03c1e1d339f 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -191,6 +191,7 @@ obj-$(CONFIG_SENSORS_SMSC47B397)+= smsc47b397.o
 obj-$(CONFIG_SENSORS_SMSC47M1)	+= smsc47m1.o
 obj-$(CONFIG_SENSORS_SMSC47M192)+= smsc47m192.o
 obj-$(CONFIG_SENSORS_SPARX5)	+= sparx5-temp.o
+obj-$(CONFIG_SENSORS_STEAMDECK) += steamdeck-hwmon.o
 obj-$(CONFIG_SENSORS_STTS751)	+= stts751.o
 obj-$(CONFIG_SENSORS_SY7636A)	+= sy7636a-hwmon.o
 obj-$(CONFIG_SENSORS_AMC6821)	+= amc6821.o
diff --git a/drivers/hwmon/steamdeck-hwmon.c b/drivers/hwmon/steamdeck-hwmon.c
new file mode 100644
index 000000000000..9d0a5471b181
--- /dev/null
+++ b/drivers/hwmon/steamdeck-hwmon.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Steam Deck EC sensors driver
+ *
+ * Copyright (C) 2021-2022 Valve Corporation
+ */
+
+#include <linux/acpi.h>
+#include <linux/hwmon.h>
+#include <linux/platform_device.h>
+
+#define STEAMDECK_HWMON_NAME	"steamdeck-hwmon"
+
+struct steamdeck_hwmon {
+	struct acpi_device *adev;
+};
+
+static long
+steamdeck_hwmon_get(struct steamdeck_hwmon *sd, const char *method)
+{
+	unsigned long long val;
+	if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle,
+					       (char *)method, NULL, &val)))
+		return -EIO;
+
+	return val;
+}
+
+static int
+steamdeck_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+		     u32 attr, int channel, long *out)
+{
+	struct steamdeck_hwmon *sd = dev_get_drvdata(dev);
+
+	switch (type) {
+	case hwmon_curr:
+		if (attr != hwmon_curr_input)
+			return -EOPNOTSUPP;
+
+		*out = steamdeck_hwmon_get(sd, "PDAM");
+		if (*out < 0)
+			return *out;
+		break;
+	case hwmon_in:
+		if (attr != hwmon_in_input)
+			return -EOPNOTSUPP;
+
+		*out = steamdeck_hwmon_get(sd, "PDVL");
+		if (*out < 0)
+			return *out;
+		break;
+	case hwmon_temp:
+		if (attr != hwmon_temp_input)
+			return -EOPNOTSUPP;
+
+		*out = steamdeck_hwmon_get(sd, "BATT");
+		if (*out < 0)
+			return *out;
+		/*
+		 * Assuming BATT returns deg C we need to mutiply it
+		 * by 1000 to convert to mC
+		 */
+		*out *= 1000;
+		break;
+	case hwmon_fan:
+		switch (attr) {
+		case hwmon_fan_input:
+			*out = steamdeck_hwmon_get(sd, "FANR");
+			if (*out < 0)
+				return *out;
+			break;
+		case hwmon_fan_target:
+			*out = steamdeck_hwmon_get(sd, "FSSR");
+			if (*out < 0)
+				return *out;
+			break;
+		case hwmon_fan_fault:
+			*out = steamdeck_hwmon_get(sd, "FANC");
+			if (*out < 0)
+				return *out;
+			/*
+			 * FANC (Fan check):
+			 * 0: Abnormal
+			 * 1: Normal
+			 */
+			*out = !*out;
+			break;
+		default:
+			return -EOPNOTSUPP;
+		}
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return 0;
+}
+
+static int
+steamdeck_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type,
+			    u32 attr, int channel, const char **str)
+{
+	switch (type) {
+		/*
+		 * These two aren't, strictly speaking, measured. EC
+		 * firmware just reports what PD negotiation resulted
+		 * in.
+		 */
+	case hwmon_curr:
+		*str = "PD Contract Current";
+		break;
+	case hwmon_in:
+		*str = "PD Contract Voltage";
+		break;
+	case hwmon_temp:
+		*str = "Battery Temp";
+		break;
+	case hwmon_fan:
+		*str = "System Fan";
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return 0;
+}
+
+static int
+steamdeck_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
+		      u32 attr, int channel, long val)
+{
+	struct steamdeck_hwmon *sd = dev_get_drvdata(dev);
+
+	if (type != hwmon_fan ||
+	    attr != hwmon_fan_target)
+		return -EOPNOTSUPP;
+
+	val = clamp_val(val, 0, 7300);
+
+	if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle,
+						    "FANS", val)))
+		return -EIO;
+
+	return 0;
+}
+
+static umode_t
+steamdeck_hwmon_is_visible(const void *data, enum hwmon_sensor_types type,
+			   u32 attr, int channel)
+{
+	if (type == hwmon_fan &&
+	    attr == hwmon_fan_target)
+		return 0644;
+
+	return 0444;
+}
+
+static const struct hwmon_channel_info *steamdeck_hwmon_info[] = {
+	HWMON_CHANNEL_INFO(in,
+			   HWMON_I_INPUT | HWMON_I_LABEL),
+	HWMON_CHANNEL_INFO(curr,
+			   HWMON_C_INPUT | HWMON_C_LABEL),
+	HWMON_CHANNEL_INFO(temp,
+			   HWMON_T_INPUT | HWMON_T_LABEL),
+	HWMON_CHANNEL_INFO(fan,
+			   HWMON_F_INPUT | HWMON_F_LABEL |
+			   HWMON_F_TARGET | HWMON_F_FAULT),
+	NULL
+};
+
+static const struct hwmon_ops steamdeck_hwmon_ops = {
+	.is_visible = steamdeck_hwmon_is_visible,
+	.read = steamdeck_hwmon_read,
+	.read_string = steamdeck_hwmon_read_string,
+	.write = steamdeck_hwmon_write,
+};
+
+static const struct hwmon_chip_info steamdeck_hwmon_chip_info = {
+	.ops = &steamdeck_hwmon_ops,
+	.info = steamdeck_hwmon_info,
+};
+
+
+static ssize_t
+steamdeck_hwmon_simple_store(struct device *dev, const char *buf, size_t count,
+			     const char *method,
+			     unsigned long upper_limit)
+{
+	struct steamdeck_hwmon *sd = dev_get_drvdata(dev);
+	unsigned long value;
+
+	if (kstrtoul(buf, 10, &value) || value >= upper_limit)
+		return -EINVAL;
+
+	if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle,
+						    (char *)method, value)))
+		return -EIO;
+
+	return count;
+}
+
+static ssize_t
+steamdeck_hwmon_simple_show(struct device *dev, char *buf,
+			    const char *method)
+{
+	struct steamdeck_hwmon *sd = dev_get_drvdata(dev);
+	unsigned long value;
+
+	value = steamdeck_hwmon_get(sd, method);
+	if (value < 0)
+		return value;
+
+	return sprintf(buf, "%ld\n", value);
+}
+
+#define STEAMDECK_HWMON_ATTR_RW(_name, _set_method, _get_method,	\
+				_upper_limit)				\
+	static ssize_t _name##_show(struct device *dev,			\
+				    struct device_attribute *attr,	\
+				    char *buf)				\
+	{								\
+		return steamdeck_hwmon_simple_show(dev, buf,		\
+						   _get_method);	\
+	}								\
+	static ssize_t _name##_store(struct device *dev,		\
+				     struct device_attribute *attr,	\
+				     const char *buf, size_t count)	\
+	{								\
+		return steamdeck_hwmon_simple_store(dev, buf, count,	\
+						    _set_method,	\
+						    _upper_limit);	\
+	}								\
+	static DEVICE_ATTR_RW(_name)
+
+STEAMDECK_HWMON_ATTR_RW(max_battery_charge_level, "FCBL", "SFBL", 101);
+STEAMDECK_HWMON_ATTR_RW(max_battery_charge_rate,  "CHGR", "GCHR", 101);
+
+static struct attribute *steamdeck_hwmon_attributes[] = {
+	&dev_attr_max_battery_charge_level.attr,
+	&dev_attr_max_battery_charge_rate.attr,
+	NULL
+};
+
+static const struct attribute_group steamdeck_hwmon_group = {
+	.attrs = steamdeck_hwmon_attributes,
+};
+
+static const struct attribute_group *steamdeck_hwmon_groups[] = {
+	&steamdeck_hwmon_group,
+	NULL
+};
+
+static int steamdeck_hwmon_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct steamdeck_hwmon *sd;
+	struct device *hwmon;
+
+	sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+
+	sd->adev = ACPI_COMPANION(dev->parent);
+	hwmon = devm_hwmon_device_register_with_info(dev,
+						     "steamdeck_hwmon",
+						     sd,
+						     &steamdeck_hwmon_chip_info,
+						     steamdeck_hwmon_groups);
+	if (IS_ERR(hwmon)) {
+		dev_err(dev, "Failed to register HWMON device");
+		return PTR_ERR(hwmon);
+	}
+
+	return 0;
+}
+
+static const struct platform_device_id steamdeck_hwmon_id_table[] = {
+	{ .name = STEAMDECK_HWMON_NAME },
+	{}
+};
+MODULE_DEVICE_TABLE(platform, steamdeck_hwmon_id_table);
+
+static struct platform_driver steamdeck_hwmon_driver = {
+	.probe = steamdeck_hwmon_probe,
+	.driver = {
+		.name = STEAMDECK_HWMON_NAME,
+	},
+	.id_table = steamdeck_hwmon_id_table,
+};
+module_platform_driver(steamdeck_hwmon_driver);
+
+MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
+MODULE_DESCRIPTION("Steam Deck EC sensors driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 2378cfb7443e..fb49f0c1b50b 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -864,6 +864,13 @@ config LEDS_ACER_A500
 	  This option enables support for the Power Button LED of
 	  Acer Iconia Tab A500.
 
+config LEDS_STEAMDECK
+	tristate "LED support for Steam Deck"
+	depends on LEDS_CLASS && MFD_STEAMDECK
+	help
+	  This option enabled support for the status LED (next to the
+	  power button) on Steam Deck
+
 source "drivers/leds/blink/Kconfig"
 
 comment "Flash and Torch LED drivers"
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 4fd2f92cd198..130a1c175dde 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_LEDS_PWM)			+= leds-pwm.o
 obj-$(CONFIG_LEDS_REGULATOR)		+= leds-regulator.o
 obj-$(CONFIG_LEDS_S3C24XX)		+= leds-s3c24xx.o
 obj-$(CONFIG_LEDS_SC27XX_BLTC)		+= leds-sc27xx-bltc.o
+obj-$(CONFIG_LEDS_STEAMDECK)		+= leds-steamdeck.o
 obj-$(CONFIG_LEDS_SUNFIRE)		+= leds-sunfire.o
 obj-$(CONFIG_LEDS_SYSCON)		+= leds-syscon.o
 obj-$(CONFIG_LEDS_TCA6507)		+= leds-tca6507.o
diff --git a/drivers/leds/leds-steamdeck.c b/drivers/leds/leds-steamdeck.c
new file mode 100644
index 000000000000..686500b8de73
--- /dev/null
+++ b/drivers/leds/leds-steamdeck.c
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/*
+ * Steam Deck EC MFD LED cell driver
+ *
+ * Copyright (C) 2021-2022 Valve Corporation
+ *
+ */
+
+#include <linux/acpi.h>
+#include <linux/leds.h>
+#include <linux/platform_device.h>
+
+struct steamdeck_led {
+	struct acpi_device *adev;
+	struct led_classdev cdev;
+};
+
+static int steamdeck_leds_brightness_set(struct led_classdev *cdev,
+					 enum led_brightness value)
+{
+	struct steamdeck_led *sd = container_of(cdev, struct steamdeck_led,
+						cdev);
+
+	if (ACPI_FAILURE(acpi_execute_simple_method(sd->adev->handle,
+						    "CHBV", value)))
+		return -EIO;
+
+	return 0;
+}
+
+static int steamdeck_leds_probe(struct platform_device *pdev)
+{
+  	struct device *dev = &pdev->dev;
+	struct steamdeck_led *sd;
+	int ret;
+
+	sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+
+	sd->adev = ACPI_COMPANION(dev->parent);
+
+	sd->cdev.name = "status:white";
+	sd->cdev.brightness_set_blocking = steamdeck_leds_brightness_set;
+	sd->cdev.max_brightness = 100;
+
+	ret = devm_led_classdev_register(dev, &sd->cdev);
+	if (ret) {
+		dev_err(dev, "Failed to register LEDs device: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct platform_device_id steamdeck_leds_id_table[] = {
+	{ .name = "steamdeck-leds" },
+	{}
+};
+MODULE_DEVICE_TABLE(platform, steamdeck_leds_id_table);
+
+static struct platform_driver steamdeck_leds_driver = {
+	.probe = steamdeck_leds_probe,
+	.driver = {
+		.name = "steamdeck-leds",
+	},
+	.id_table = steamdeck_leds_id_table,
+};
+module_platform_driver(steamdeck_leds_driver);
+
+MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
+MODULE_DESCRIPTION("Steam Deck LEDs driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 9da8235cb690..35bf61d8f399 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2262,5 +2262,16 @@ config MFD_RSMU_SPI
 	  Additional drivers must be enabled in order to use the functionality
 	  of the device.
 
+config MFD_STEAMDECK
+	tristate "Valve Steam Deck"
+	select MFD_CORE
+	depends on ACPI
+	depends on X86_64 || COMPILE_TEST
+	help
+	  This driver registers various MFD cells that expose aspects
+	  of Steam Deck specific ACPI functionality.
+
+	  Say N here, unless you are running on Steam Deck hardware.
+
 endmenu
 endif
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index 7ed3ef4a698c..d01254ef0106 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -280,3 +280,5 @@ rsmu-i2c-objs			:= rsmu_core.o rsmu_i2c.o
 rsmu-spi-objs			:= rsmu_core.o rsmu_spi.o
 obj-$(CONFIG_MFD_RSMU_I2C)	+= rsmu-i2c.o
 obj-$(CONFIG_MFD_RSMU_SPI)	+= rsmu-spi.o
+
+obj-$(CONFIG_MFD_STEAMDECK)	+= steamdeck.o
diff --git a/drivers/mfd/steamdeck.c b/drivers/mfd/steamdeck.c
new file mode 100644
index 000000000000..0e504b3c2796
--- /dev/null
+++ b/drivers/mfd/steamdeck.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/*
+ * Steam Deck EC MFD core driver
+ *
+ * Copyright (C) 2021-2022 Valve Corporation
+ *
+ */
+
+#include <linux/acpi.h>
+#include <linux/platform_device.h>
+#include <linux/mfd/core.h>
+
+#define STEAMDECK_STA_OK			\
+	(ACPI_STA_DEVICE_ENABLED |		\
+	 ACPI_STA_DEVICE_PRESENT |		\
+	 ACPI_STA_DEVICE_FUNCTIONING)
+
+struct steamdeck {
+	struct acpi_device *adev;
+	struct device *dev;
+};
+
+#define STEAMDECK_ATTR_RO(_name, _method)				\
+	static ssize_t _name##_show(struct device *dev,			\
+				    struct device_attribute *attr,	\
+				    char *buf)				\
+	{								\
+		struct steamdeck *sd = dev_get_drvdata(dev);		\
+		unsigned long long val;					\
+									\
+		if (ACPI_FAILURE(acpi_evaluate_integer(			\
+					 sd->adev->handle,		\
+					 _method, NULL, &val)))		\
+			return -EIO;					\
+									\
+		return sysfs_emit(buf, "%llu\n", val);			\
+	}								\
+	static DEVICE_ATTR_RO(_name)
+
+STEAMDECK_ATTR_RO(firmware_version, "PDFW");
+STEAMDECK_ATTR_RO(board_id, "BOID");
+
+static struct attribute *steamdeck_attrs[] = {
+	&dev_attr_firmware_version.attr,
+	&dev_attr_board_id.attr,
+	NULL
+};
+
+ATTRIBUTE_GROUPS(steamdeck);
+
+static const struct mfd_cell steamdeck_cells[] = {
+	{ .name = "steamdeck-hwmon"  },
+	{ .name = "steamdeck-leds"   },
+	{ .name = "steamdeck-extcon" },
+};
+
+static void steamdeck_remove_sysfs_groups(void *data)
+{
+	struct steamdeck *sd = data;
+
+	sysfs_remove_groups(&sd->dev->kobj, steamdeck_groups);
+}
+
+static int steamdeck_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	unsigned long long sta;
+	struct steamdeck *sd;
+	acpi_status status;
+	int ret;
+
+	sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
+	if (!sd)
+		return -ENOMEM;
+	sd->adev = ACPI_COMPANION(dev);
+	sd->dev = dev;
+	platform_set_drvdata(pdev, sd);
+
+	status = acpi_evaluate_integer(sd->adev->handle, "_STA",
+				       NULL, &sta);
+	if (ACPI_FAILURE(status)) {
+		dev_err(dev, "Status check failed (0x%x)\n", status);
+		return -EINVAL;
+	}
+
+	if ((sta & STEAMDECK_STA_OK) != STEAMDECK_STA_OK) {
+		dev_err(dev, "Device is not ready\n");
+		return -EINVAL;
+	}
+
+	ret = sysfs_create_groups(&dev->kobj, steamdeck_groups);
+	if (ret) {
+		dev_err(dev, "Failed to create sysfs group\n");
+		return ret;
+	}
+
+	ret = devm_add_action_or_reset(dev, steamdeck_remove_sysfs_groups,
+				       sd);
+	if (ret) {
+		dev_err(dev, "Failed to register devres action\n");
+		return ret;
+	}
+
+	return devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE,
+				    steamdeck_cells, ARRAY_SIZE(steamdeck_cells),
+				    NULL, 0, NULL);
+}
+
+static const struct acpi_device_id steamdeck_device_ids[] = {
+	{ "VLV0100", 0 },
+	{ "", 0 },
+};
+MODULE_DEVICE_TABLE(acpi, steamdeck_device_ids);
+
+static struct platform_driver steamdeck_driver = {
+	.probe = steamdeck_probe,
+	.driver = {
+		.name = "steamdeck",
+		.acpi_match_table = steamdeck_device_ids,
+	},
+};
+module_platform_driver(steamdeck_driver);
+
+MODULE_AUTHOR("Andrey Smirnov <andrew.smirnov@gmail.com>");
+MODULE_DESCRIPTION("Steam Deck EC MFD core driver");
+MODULE_LICENSE("GPL");