summaryrefslogtreecommitdiff
path: root/SOURCES/steam-deck.patch
diff options
context:
space:
mode:
Diffstat (limited to 'SOURCES/steam-deck.patch')
-rw-r--r--SOURCES/steam-deck.patch598
1 files changed, 598 insertions, 0 deletions
diff --git a/SOURCES/steam-deck.patch b/SOURCES/steam-deck.patch
new file mode 100644
index 0000000..1224d2b
--- /dev/null
+++ b/SOURCES/steam-deck.patch
@@ -0,0 +1,598 @@
+
+Add a driver exposing various bits and pieces of functionality
+provided by Steam Deck specific VLV0100 device presented by EC
+firmware. This includes but not limited to:
+
+ - CPU/device's fan control
+ - Read-only access to DDIC registers
+ - Battery tempreature measurements
+ - Various display related control knobs
+ - USB Type-C connector event notification
+
+Cc: Hans de Goede <hdegoede@redhat.com>
+Cc: Mark Gross <markgross@kernel.org>
+Cc: Jean Delvare <jdelvare@suse.com>
+Cc: Guenter Roeck <linux@roeck-us.net>
+Cc: linux-kernel@vger.kernel.org (open list)
+Cc: platform-driver-x86@vger.kernel.org
+Cc: linux-hwmon@vger.kernel.org
+Signed-off-by: Andrey Smirnov <andrew.smirnov@gmail.com>
+---
+
+This driver is really a kitchen sink of various small bits. Maybe it
+is worth splitting into an MFD + child drivers/devices?
+
+ drivers/platform/x86/Kconfig | 15 +
+ drivers/platform/x86/Makefile | 2 +
+ drivers/platform/x86/steamdeck.c | 523 +++++++++++++++++++++++++++++++
+ 3 files changed, 540 insertions(+)
+ create mode 100644 drivers/platform/x86/steamdeck.c
+
+diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
+index c23612d98126..86f014e78a6e 100644
+--- a/drivers/platform/x86/Kconfig
++++ b/drivers/platform/x86/Kconfig
+@@ -1075,6 +1075,20 @@
+ low level access for debug work and updating the firmware. Say
+ N unless you will be doing this on an Intel MID platform.
+
++config STEAMDECK
++ tristate "Valve Steam Deck platform driver"
++ depends on X86_64
++ help
++ Driver exposing various bits and pieces of functionality
++ provided by Steam Deck specific VLV0100 device presented by
++ EC firmware. This includes but not limited to:
++ - CPU/device's fan control
++ - Read-only access to DDIC registers
++ - Battery tempreature measurements
++ - Various display related control knobs
++ - USB Type-C connector event notification
++ Say N unless you are running on a Steam Deck.
++
+ endif # X86_PLATFORM_DEVICES
+
+ config PMC_ATOM
+
+diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
+index c12a9b044fd8..2eb965e14ced 100644
+--- a/drivers/platform/x86/Makefile
++++ b/drivers/platform/x86/Makefile
+@@ -123,3 +123,6 @@
+ obj-$(CONFIG_INTEL_SCU_WDT) += intel_scu_wdt.o
+ obj-$(CONFIG_INTEL_SCU_IPC_UTIL) += intel_scu_ipcutil.o
+ obj-$(CONFIG_PMC_ATOM) += pmc_atom.o
++
++# Steam Deck
++obj-$(CONFIG_STEAMDECK) += steamdeck.o
+diff --git a/drivers/platform/x86/steamdeck.c b/drivers/platform/x86/steamdeck.c
+new file mode 100644
+index 000000000000..77a6677ec19e
+--- /dev/null
++++ b/drivers/platform/x86/steamdeck.c
+@@ -0,0 +1,523 @@
++// SPDX-License-Identifier: GPL-2.0+
++
++/*
++ * Steam Deck ACPI platform driver
++ *
++ * Copyright (C) 2021-2022 Valve Corporation
++ *
++ */
++#include <linux/acpi.h>
++#include <linux/hwmon.h>
++#include <linux/platform_device.h>
++#include <linux/regmap.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 {
++ struct acpi_device *adev;
++ struct device *hwmon;
++ void *regmap;
++ long fan_target;
++ struct delayed_work role_work;
++ struct extcon_dev *edev;
++ struct device *dev;
++};
++
++static ssize_t
++steamdeck_simple_store(struct device *dev, const char *buf, size_t count,
++ const char *method,
++ unsigned long upper_limit)
++{
++ struct steamdeck *fan = dev_get_drvdata(dev);
++ unsigned long value;
++
++ if (kstrtoul(buf, 10, &value) || value >= upper_limit)
++ return -EINVAL;
++
++ if (ACPI_FAILURE(acpi_execute_simple_method(fan->adev->handle,
++ (char *)method, value)))
++ return -EIO;
++
++ return count;
++}
++
++#define STEAMDECK_ATTR_WO(_name, _method, _upper_limit) \
++ static ssize_t _name##_store(struct device *dev, \
++ struct device_attribute *attr, \
++ const char *buf, size_t count) \
++ { \
++ return steamdeck_simple_store(dev, buf, count, \
++ _method, \
++ _upper_limit); \
++ } \
++ static DEVICE_ATTR_WO(_name)
++
++STEAMDECK_ATTR_WO(target_cpu_temp, "STCT", U8_MAX / 2);
++STEAMDECK_ATTR_WO(gain, "SGAN", U16_MAX);
++STEAMDECK_ATTR_WO(ramp_rate, "SFRR", U8_MAX);
++STEAMDECK_ATTR_WO(hysteresis, "SHTS", U16_MAX);
++STEAMDECK_ATTR_WO(maximum_battery_charge_rate, "CHGR", U16_MAX);
++STEAMDECK_ATTR_WO(recalculate, "SCHG", U16_MAX);
++
++STEAMDECK_ATTR_WO(led_brightness, "CHBV", U8_MAX);
++STEAMDECK_ATTR_WO(content_adaptive_brightness, "CABC", U8_MAX);
++STEAMDECK_ATTR_WO(gamma_set, "GAMA", U8_MAX);
++STEAMDECK_ATTR_WO(display_brightness, "WDBV", U8_MAX);
++STEAMDECK_ATTR_WO(ctrl_display, "WCDV", U8_MAX);
++STEAMDECK_ATTR_WO(cabc_minimum_brightness, "WCMB", U8_MAX);
++STEAMDECK_ATTR_WO(memory_data_access_control, "MDAC", U8_MAX);
++
++#define STEAMDECK_ATTR_WO_NOARG(_name, _method) \
++ static ssize_t _name##_store(struct device *dev, \
++ struct device_attribute *attr, \
++ const char *buf, size_t count) \
++ { \
++ struct steamdeck *fan = dev_get_drvdata(dev); \
++ \
++ if (ACPI_FAILURE(acpi_evaluate_object(fan->adev->handle, \
++ _method, NULL, NULL))) \
++ return -EIO; \
++ \
++ return count; \
++ } \
++ static DEVICE_ATTR_WO(_name)
++
++STEAMDECK_ATTR_WO_NOARG(power_cycle_display, "DPCY");
++STEAMDECK_ATTR_WO_NOARG(display_normal_mode_on, "NORO");
++STEAMDECK_ATTR_WO_NOARG(display_inversion_off, "INOF");
++STEAMDECK_ATTR_WO_NOARG(display_inversion_on, "INON");
++STEAMDECK_ATTR_WO_NOARG(idle_mode_on, "WRNE");
++
++#define STEAMDECK_ATTR_RO(_name, _method) \
++ static ssize_t _name##_show(struct device *dev, \
++ struct device_attribute *attr, \
++ char *buf) \
++ { \
++ struct steamdeck *jup = dev_get_drvdata(dev); \
++ unsigned long long val; \
++ \
++ if (ACPI_FAILURE(acpi_evaluate_integer( \
++ jup->adev->handle, \
++ _method, NULL, &val))) \
++ return -EIO; \
++ \
++ return sprintf(buf, "%llu\n", val); \
++ } \
++ static DEVICE_ATTR_RO(_name)
++
++STEAMDECK_ATTR_RO(firmware_version, "PDFW");
++STEAMDECK_ATTR_RO(board_id, "BOID");
++STEAMDECK_ATTR_RO(pdcs, "PDCS");
++
++static umode_t
++steamdeck_is_visible(struct kobject *kobj, struct attribute *attr, int index)
++{
++ return attr->mode;
++}
++
++static struct attribute *steamdeck_attributes[] = {
++ &dev_attr_target_cpu_temp.attr,
++ &dev_attr_gain.attr,
++ &dev_attr_ramp_rate.attr,
++ &dev_attr_hysteresis.attr,
++ &dev_attr_maximum_battery_charge_rate.attr,
++ &dev_attr_recalculate.attr,
++ &dev_attr_power_cycle_display.attr,
++
++ &dev_attr_led_brightness.attr,
++ &dev_attr_content_adaptive_brightness.attr,
++ &dev_attr_gamma_set.attr,
++ &dev_attr_display_brightness.attr,
++ &dev_attr_ctrl_display.attr,
++ &dev_attr_cabc_minimum_brightness.attr,
++ &dev_attr_memory_data_access_control.attr,
++
++ &dev_attr_display_normal_mode_on.attr,
++ &dev_attr_display_inversion_off.attr,
++ &dev_attr_display_inversion_on.attr,
++ &dev_attr_idle_mode_on.attr,
++
++ &dev_attr_firmware_version.attr,
++ &dev_attr_board_id.attr,
++ &dev_attr_pdcs.attr,
++
++ NULL
++};
++
++static const struct attribute_group steamdeck_group = {
++ .attrs = steamdeck_attributes,
++ .is_visible = steamdeck_is_visible,
++};
++
++static const struct attribute_group *steamdeck_groups[] = {
++ &steamdeck_group,
++ NULL
++};
++
++static int steamdeck_read_fan_speed(struct steamdeck *jup, long *speed)
++{
++ unsigned long long val;
++
++ if (ACPI_FAILURE(acpi_evaluate_integer(jup->adev->handle,
++ "FANR", NULL, &val)))
++ return -EIO;
++
++ *speed = val;
++ return 0;
++}
++
++static int
++steamdeck_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
++ u32 attr, int channel, long *out)
++{
++ struct steamdeck *sd = dev_get_drvdata(dev);
++ unsigned long long val;
++
++ switch (type) {
++ case hwmon_temp:
++ if (attr != hwmon_temp_input)
++ return -EOPNOTSUPP;
++
++ if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle,
++ "BATT", NULL, &val)))
++ return -EIO;
++ /*
++ * Assuming BATT returns deg C we need to mutiply it
++ * by 1000 to convert to mC
++ */
++ *out = val * 1000;
++ break;
++ case hwmon_fan:
++ switch (attr) {
++ case hwmon_fan_input:
++ return steamdeck_read_fan_speed(sd, out);
++ case hwmon_fan_target:
++ *out = sd->fan_target;
++ break;
++ case hwmon_fan_fault:
++ if (ACPI_FAILURE(acpi_evaluate_integer(
++ sd->adev->handle,
++ "FANC", NULL, &val)))
++ return -EIO;
++ /*
++ * FANC (Fan check):
++ * 0: Abnormal
++ * 1: Normal
++ */
++ *out = !val;
++ 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) {
++ 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 *sd = dev_get_drvdata(dev);
++
++ if (type != hwmon_fan ||
++ attr != hwmon_fan_target)
++ return -EOPNOTSUPP;
++
++ if (val > U16_MAX)
++ return -EINVAL;
++
++ sd->fan_target = val;
++
++ 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_info[] = {
++ 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_chip_info = {
++ .ops = &steamdeck_hwmon_ops,
++ .info = steamdeck_info,
++};
++
++#define STEAMDECK_STA_OK \
++ (ACPI_STA_DEVICE_ENABLED | \
++ ACPI_STA_DEVICE_PRESENT | \
++ ACPI_STA_DEVICE_FUNCTIONING)
++
++static int
++steamdeck_ddic_reg_read(void *context, unsigned int reg, unsigned int *val)
++{
++ union acpi_object obj = { .type = ACPI_TYPE_INTEGER };
++ struct acpi_object_list arg_list = { .count = 1, .pointer = &obj, };
++ struct steamdeck *sd = context;
++ unsigned long long _val;
++
++ obj.integer.value = reg;
++
++ if (ACPI_FAILURE(acpi_evaluate_integer(sd->adev->handle,
++ "RDDI", &arg_list, &_val)))
++ return -EIO;
++
++ *val = _val;
++ return 0;
++}
++
++static int steamdeck_read_pdcs(struct steamdeck *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 *sd =
++ container_of(work, struct steamdeck, 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;
++
++ WARN_ON(extcon_set_state_sync(sd->edev, EXTCON_USB_HOST,
++ usb_host));
++ dev_dbg(sd->dev, "USB role is %s\n", usb_host ? "host" : "device");
++}
++
++static void steamdeck_notify(acpi_handle handle, u32 event, void *context)
++{
++ struct device *dev = context;
++ struct steamdeck *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_err(dev, "Unsupported event [0x%x]\n", event);
++ }
++}
++
++static void steamdeck_remove_notify_handler(void *data)
++{
++ struct steamdeck *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_probe(struct platform_device *pdev)
++{
++ struct device *dev = &pdev->dev;
++ struct steamdeck *sd;
++ acpi_status status;
++ unsigned long long sta;
++ int ret;
++
++ static const struct regmap_config regmap_config = {
++ .reg_bits = 8,
++ .val_bits = 8,
++ .max_register = 255,
++ .cache_type = REGCACHE_NONE,
++ .reg_read = steamdeck_ddic_reg_read,
++ };
++
++ sd = devm_kzalloc(dev, sizeof(*sd), GFP_KERNEL);
++ if (!sd)
++ return -ENOMEM;
++ sd->adev = ACPI_COMPANION(&pdev->dev);
++ sd->dev = dev;
++ platform_set_drvdata(pdev, sd);
++ INIT_DELAYED_WORK(&sd->role_work, steamdeck_usb_role_work);
++
++ 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;
++ }
++
++ /*
++ * Our ACPI interface doesn't expose a method to read current
++ * fan target, so we use current fan speed as an
++ * approximation.
++ */
++ if (steamdeck_read_fan_speed(sd, &sd->fan_target))
++ dev_warn(dev, "Failed to read fan speed");
++
++ sd->hwmon = devm_hwmon_device_register_with_info(dev,
++ "steamdeck",
++ sd,
++ &steamdeck_chip_info,
++ steamdeck_groups);
++ if (IS_ERR(sd->hwmon)) {
++ dev_err(dev, "Failed to register HWMON device");
++ return PTR_ERR(sd->hwmon);
++ }
++
++ sd->regmap = devm_regmap_init(dev, NULL, sd, &regmap_config);
++ if (IS_ERR(sd->regmap))
++ dev_err(dev, "Failed to register REGMAP");
++
++ sd->edev = devm_extcon_dev_allocate(dev, steamdeck_extcon_cable);
++ if (IS_ERR(sd->edev))
++ return -ENOMEM;
++
++ 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 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 ACPI platform driver");
++MODULE_LICENSE("GPL");
+--
+2.25.1