Hi All, On 2/17/22 17:26, Hans de Goede wrote: > Hi Andrey, > > On 2/6/22 03:20, Andrey Smirnov wrote: >> 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@xxxxxxxxxx> >> Cc: Mark Gross <markgross@xxxxxxxxxx> >> Cc: Jean Delvare <jdelvare@xxxxxxxx> >> Cc: Guenter Roeck <linux@xxxxxxxxxxxx> >> Cc: linux-kernel@xxxxxxxxxxxxxxx (open list) >> Cc: platform-driver-x86@xxxxxxxxxxxxxxx >> Cc: linux-hwmon@xxxxxxxxxxxxxxx >> Signed-off-by: Andrey Smirnov <andrew.smirnov@xxxxxxxxx> > > The .c file says: "Copyright (C) 2021-2022 Valve Corporation" > yet you are using a personal email address. This is not really > an issue, but it does look a bit weird. > >> --- >> >> This driver is really a kitchen sink of various small bits. Maybe it >> is worth splitting into an MFD + child drivers/devices? > > Yes with the extcon thing I think you should definitely go for > a MFD device. In which case the main driver registering the > regmap + the cells would go under drivers/mfd and most of the > other drivers would go in their own subsystems. > > And as the drivers/platform/x86/ subsystem maintainer I guess > that means I don't have to do much with this driver :) > > I would still be happy to take any bits which don't fit > anywhere else attaching to say a "misc" MFD cell. > > Regards, > > Hans Sorry for sending an almost identical mail twice, at first I thought my email-client (thunderbird) somehow ate this one, as it did not show in my send folder. But now I see that I somehow accidentally changed from which account I send the reply. Regards, Hans > > > >> >> 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 >> @@ -1136,6 +1136,21 @@ config SIEMENS_SIMATIC_IPC >> To compile this driver as a module, choose M here: the module >> will be called simatic-ipc. >> >> +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 >> @@ -129,3 +129,5 @@ obj-$(CONFIG_PMC_ATOM) += pmc_atom.o >> >> # Siemens Simatic Industrial PCs >> obj-$(CONFIG_SIEMENS_SIMATIC_IPC) += simatic-ipc.o >> + >> +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, ®map_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@xxxxxxxxx>"); >> +MODULE_DESCRIPTION("Steam Deck ACPI platform driver"); >> +MODULE_LICENSE("GPL"); >> -- >> 2.25.1 >> >