// SPDX-License-Identifier: GPL-2.0-or-later /* * Linux driver for WMI sensor information on Dell notebooks. * * Copyright (C) 2022 Armin Wolf */ #define pr_format(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DRIVER_NAME "dell-wmi-ddv" #define DELL_DDV_SUPPORTED_VERSION_MIN 2 #define DELL_DDV_SUPPORTED_VERSION_MAX 3 #define DELL_DDV_GUID "8A42EA14-4F2A-FD45-6422-0087F7A7E608" #define DELL_EPPID_LENGTH 20 #define DELL_EPPID_EXT_LENGTH 23 static bool force; module_param_unsafe(force, bool, 0); MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions"); enum dell_ddv_method { DELL_DDV_BATTERY_DESIGN_CAPACITY = 0x01, DELL_DDV_BATTERY_FULL_CHARGE_CAPACITY = 0x02, DELL_DDV_BATTERY_MANUFACTURE_NAME = 0x03, DELL_DDV_BATTERY_MANUFACTURE_DATE = 0x04, DELL_DDV_BATTERY_SERIAL_NUMBER = 0x05, DELL_DDV_BATTERY_CHEMISTRY_VALUE = 0x06, DELL_DDV_BATTERY_TEMPERATURE = 0x07, DELL_DDV_BATTERY_CURRENT = 0x08, DELL_DDV_BATTERY_VOLTAGE = 0x09, DELL_DDV_BATTERY_MANUFACTURER_ACCESS = 0x0A, DELL_DDV_BATTERY_RELATIVE_CHARGE_STATE = 0x0B, DELL_DDV_BATTERY_CYCLE_COUNT = 0x0C, DELL_DDV_BATTERY_EPPID = 0x0D, DELL_DDV_BATTERY_RAW_ANALYTICS_START = 0x0E, DELL_DDV_BATTERY_RAW_ANALYTICS = 0x0F, DELL_DDV_BATTERY_DESIGN_VOLTAGE = 0x10, DELL_DDV_BATTERY_RAW_ANALYTICS_A_BLOCK = 0x11, /* version 3 */ DELL_DDV_INTERFACE_VERSION = 0x12, DELL_DDV_FAN_SENSOR_INFORMATION = 0x20, DELL_DDV_THERMAL_SENSOR_INFORMATION = 0x22, }; struct fan_sensor_entry { u8 type; __le16 rpm; } __packed; struct thermal_sensor_entry { u8 type; s8 now; s8 min; s8 max; u8 unknown; } __packed; struct combined_channel_info { struct hwmon_channel_info info; u32 config[]; }; struct combined_chip_info { struct hwmon_chip_info chip; const struct hwmon_channel_info *info[]; }; struct dell_wmi_ddv_sensors { bool active; struct mutex lock; /* protect caching */ unsigned long timestamp; union acpi_object *obj; u64 entries; }; struct dell_wmi_ddv_data { struct acpi_battery_hook hook; struct device_attribute temp_attr; struct device_attribute eppid_attr; struct dell_wmi_ddv_sensors fans; struct dell_wmi_ddv_sensors temps; struct wmi_device *wdev; }; static const char * const fan_labels[] = { "CPU Fan", "Chassis Motherboard Fan", "Video Fan", "Power Supply Fan", "Chipset Fan", "Memory Fan", "PCI Fan", "HDD Fan", }; static const char * const fan_dock_labels[] = { "Docking Chassis/Motherboard Fan", "Docking Video Fan", "Docking Power Supply Fan", "Docking Chipset Fan", }; static int dell_wmi_ddv_query_type(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg, union acpi_object **result, acpi_object_type type) { struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; const struct acpi_buffer in = { .length = sizeof(arg), .pointer = &arg, }; union acpi_object *obj; acpi_status ret; ret = wmidev_evaluate_method(wdev, 0x0, method, &in, &out); if (ACPI_FAILURE(ret)) return -EIO; obj = out.pointer; if (!obj) return -ENODATA; if (obj->type != type) { kfree(obj); return -ENOMSG; } *result = obj; return 0; } static int dell_wmi_ddv_query_integer(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg, u32 *res) { union acpi_object *obj; int ret; ret = dell_wmi_ddv_query_type(wdev, method, arg, &obj, ACPI_TYPE_INTEGER); if (ret < 0) return ret; if (obj->integer.value <= U32_MAX) *res = (u32)obj->integer.value; else ret = -ERANGE; kfree(obj); return ret; } static int dell_wmi_ddv_query_buffer(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg, union acpi_object **result) { union acpi_object *obj; u64 buffer_size; int ret; ret = dell_wmi_ddv_query_type(wdev, method, arg, &obj, ACPI_TYPE_PACKAGE); if (ret < 0) return ret; if (obj->package.count != 2 || obj->package.elements[0].type != ACPI_TYPE_INTEGER || obj->package.elements[1].type != ACPI_TYPE_BUFFER) { ret = -ENOMSG; goto err_free; } buffer_size = obj->package.elements[0].integer.value; if (!buffer_size) { ret = -ENODATA; goto err_free; } if (buffer_size > obj->package.elements[1].buffer.length) { dev_warn(&wdev->dev, FW_WARN "WMI buffer size (%llu) exceeds ACPI buffer size (%d)\n", buffer_size, obj->package.elements[1].buffer.length); ret = -EMSGSIZE; goto err_free; } *result = obj; return 0; err_free: kfree(obj); return ret; } static int dell_wmi_ddv_query_string(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg, union acpi_object **result) { return dell_wmi_ddv_query_type(wdev, method, arg, result, ACPI_TYPE_STRING); } /* * Needs to be called with lock held, except during initialization. */ static int dell_wmi_ddv_update_sensors(struct wmi_device *wdev, enum dell_ddv_method method, struct dell_wmi_ddv_sensors *sensors, size_t entry_size) { u64 buffer_size, rem, entries; union acpi_object *obj; u8 *buffer; int ret; if (sensors->obj) { if (time_before(jiffies, sensors->timestamp + HZ)) return 0; kfree(sensors->obj); sensors->obj = NULL; } ret = dell_wmi_ddv_query_buffer(wdev, method, 0, &obj); if (ret < 0) return ret; /* buffer format sanity check */ buffer_size = obj->package.elements[0].integer.value; buffer = obj->package.elements[1].buffer.pointer; entries = div64_u64_rem(buffer_size, entry_size, &rem); if (rem != 1 || buffer[buffer_size - 1] != 0xff) { ret = -ENOMSG; goto err_free; } if (!entries) { ret = -ENODATA; goto err_free; } sensors->obj = obj; sensors->entries = entries; sensors->timestamp = jiffies; return 0; err_free: kfree(obj); return ret; } static umode_t dell_wmi_ddv_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr, int channel) { return 0444; } static int dell_wmi_ddv_fan_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel, long *val) { struct fan_sensor_entry *entry; int ret; ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION, &data->fans, sizeof(*entry)); if (ret < 0) return ret; if (channel >= data->fans.entries) return -ENXIO; entry = (struct fan_sensor_entry *)data->fans.obj->package.elements[1].buffer.pointer; switch (attr) { case hwmon_fan_input: *val = get_unaligned_le16(&entry[channel].rpm); return 0; default: break; } return -EOPNOTSUPP; } static int dell_wmi_ddv_temp_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel, long *val) { struct thermal_sensor_entry *entry; int ret; ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, &data->temps, sizeof(*entry)); if (ret < 0) return ret; if (channel >= data->temps.entries) return -ENXIO; entry = (struct thermal_sensor_entry *)data->temps.obj->package.elements[1].buffer.pointer; switch (attr) { case hwmon_temp_input: *val = entry[channel].now * 1000; return 0; case hwmon_temp_min: *val = entry[channel].min * 1000; return 0; case hwmon_temp_max: *val = entry[channel].max * 1000; return 0; default: break; } return -EOPNOTSUPP; } static int dell_wmi_ddv_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); int ret; switch (type) { case hwmon_fan: mutex_lock(&data->fans.lock); ret = dell_wmi_ddv_fan_read_channel(data, attr, channel, val); mutex_unlock(&data->fans.lock); return ret; case hwmon_temp: mutex_lock(&data->temps.lock); ret = dell_wmi_ddv_temp_read_channel(data, attr, channel, val); mutex_unlock(&data->temps.lock); return ret; default: break; } return -EOPNOTSUPP; } static int dell_wmi_ddv_fan_read_string(struct dell_wmi_ddv_data *data, int channel, const char **str) { struct fan_sensor_entry *entry; int ret; u8 type; ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION, &data->fans, sizeof(*entry)); if (ret < 0) return ret; if (channel >= data->fans.entries) return -ENXIO; entry = (struct fan_sensor_entry *)data->fans.obj->package.elements[1].buffer.pointer; type = entry[channel].type; switch (type) { case 0x00 ... 0x07: *str = fan_labels[type]; break; case 0x11 ... 0x14: *str = fan_dock_labels[type - 0x11]; break; default: *str = "Unknown Fan"; break; } return 0; } static int dell_wmi_ddv_temp_read_string(struct dell_wmi_ddv_data *data, int channel, const char **str) { struct thermal_sensor_entry *entry; int ret; ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, &data->temps, sizeof(*entry)); if (ret < 0) return ret; if (channel >= data->temps.entries) return -ENXIO; entry = (struct thermal_sensor_entry *)data->temps.obj->package.elements[1].buffer.pointer; switch (entry[channel].type) { case 0x00: *str = "CPU"; break; case 0x11: *str = "Video"; break; case 0x22: *str = "Memory"; /* sometimes called DIMM */ break; case 0x33: *str = "Other"; break; case 0x44: *str = "Ambient"; /* sometimes called SKIN */ break; case 0x52: *str = "SODIMM"; break; case 0x55: *str = "HDD"; break; case 0x62: *str = "SODIMM 2"; break; case 0x73: *str = "NB"; break; case 0x83: *str = "Charger"; break; case 0xbb: *str = "Memory 3"; break; default: *str = "Unknown"; break; } return 0; } static int dell_wmi_ddv_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, const char **str) { struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); int ret; switch (type) { case hwmon_fan: switch (attr) { case hwmon_fan_label: mutex_lock(&data->fans.lock); ret = dell_wmi_ddv_fan_read_string(data, channel, str); mutex_unlock(&data->fans.lock); return ret; default: break; } break; case hwmon_temp: switch (attr) { case hwmon_temp_label: mutex_lock(&data->temps.lock); ret = dell_wmi_ddv_temp_read_string(data, channel, str); mutex_unlock(&data->temps.lock); return ret; default: break; } break; default: break; } return -EOPNOTSUPP; } static const struct hwmon_ops dell_wmi_ddv_ops = { .is_visible = dell_wmi_ddv_is_visible, .read = dell_wmi_ddv_read, .read_string = dell_wmi_ddv_read_string, }; static struct hwmon_channel_info *dell_wmi_ddv_channel_create(struct device *dev, u64 count, enum hwmon_sensor_types type, u32 config) { struct combined_channel_info *cinfo; int i; cinfo = devm_kzalloc(dev, struct_size(cinfo, config, count + 1), GFP_KERNEL); if (!cinfo) return ERR_PTR(-ENOMEM); cinfo->info.type = type; cinfo->info.config = cinfo->config; for (i = 0; i < count; i++) cinfo->config[i] = config; return &cinfo->info; } static void dell_wmi_ddv_hwmon_cache_invalidate(struct dell_wmi_ddv_sensors *sensors) { if (!sensors->active) return; mutex_lock(&sensors->lock); kfree(sensors->obj); sensors->obj = NULL; mutex_unlock(&sensors->lock); } static void dell_wmi_ddv_hwmon_cache_destroy(void *data) { struct dell_wmi_ddv_sensors *sensors = data; sensors->active = false; mutex_destroy(&sensors->lock); kfree(sensors->obj); } static struct hwmon_channel_info *dell_wmi_ddv_channel_init(struct wmi_device *wdev, enum dell_ddv_method method, struct dell_wmi_ddv_sensors *sensors, size_t entry_size, enum hwmon_sensor_types type, u32 config) { struct hwmon_channel_info *info; int ret; ret = dell_wmi_ddv_update_sensors(wdev, method, sensors, entry_size); if (ret < 0) return ERR_PTR(ret); mutex_init(&sensors->lock); sensors->active = true; ret = devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors); if (ret < 0) return ERR_PTR(ret); info = dell_wmi_ddv_channel_create(&wdev->dev, sensors->entries, type, config); if (IS_ERR(info)) devm_release_action(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors); return info; } static int dell_wmi_ddv_hwmon_add(struct dell_wmi_ddv_data *data) { struct wmi_device *wdev = data->wdev; struct combined_chip_info *cinfo; struct hwmon_channel_info *info; struct device *hdev; int index = 0; int ret; if (!devres_open_group(&wdev->dev, dell_wmi_ddv_hwmon_add, GFP_KERNEL)) return -ENOMEM; cinfo = devm_kzalloc(&wdev->dev, struct_size(cinfo, info, 4), GFP_KERNEL); if (!cinfo) { ret = -ENOMEM; goto err_release; } cinfo->chip.ops = &dell_wmi_ddv_ops; cinfo->chip.info = cinfo->info; info = dell_wmi_ddv_channel_create(&wdev->dev, 1, hwmon_chip, HWMON_C_REGISTER_TZ); if (IS_ERR(info)) { ret = PTR_ERR(info); goto err_release; } cinfo->info[index] = info; index++; info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_FAN_SENSOR_INFORMATION, &data->fans, sizeof(struct fan_sensor_entry), hwmon_fan, (HWMON_F_INPUT | HWMON_F_LABEL)); if (!IS_ERR(info)) { cinfo->info[index] = info; index++; } info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, &data->temps, sizeof(struct thermal_sensor_entry), hwmon_temp, (HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX | HWMON_T_LABEL)); if (!IS_ERR(info)) { cinfo->info[index] = info; index++; } if (index < 2) { ret = -ENODEV; goto err_release; } hdev = devm_hwmon_device_register_with_info(&wdev->dev, "dell_ddv", data, &cinfo->chip, NULL); if (IS_ERR(hdev)) { ret = PTR_ERR(hdev); goto err_release; } devres_close_group(&wdev->dev, dell_wmi_ddv_hwmon_add); return 0; err_release: devres_release_group(&wdev->dev, dell_wmi_ddv_hwmon_add); return ret; } static int dell_wmi_ddv_battery_index(struct acpi_device *acpi_dev, u32 *index) { const char *uid_str; uid_str = acpi_device_uid(acpi_dev); if (!uid_str) return -ENODEV; return kstrtou32(uid_str, 10, index); } static ssize_t temp_show(struct device *dev, struct device_attribute *attr, char *buf) { struct dell_wmi_ddv_data *data = container_of(attr, struct dell_wmi_ddv_data, temp_attr); u32 index, value; int ret; ret = dell_wmi_ddv_battery_index(to_acpi_device(dev->parent), &index); if (ret < 0) return ret; ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_TEMPERATURE, index, &value); if (ret < 0) return ret; /* Use 2731 instead of 2731.5 to avoid unnecessary rounding */ return sysfs_emit(buf, "%d\n", value - 2731); } static ssize_t eppid_show(struct device *dev, struct device_attribute *attr, char *buf) { struct dell_wmi_ddv_data *data = container_of(attr, struct dell_wmi_ddv_data, eppid_attr); union acpi_object *obj; u32 index; int ret; ret = dell_wmi_ddv_battery_index(to_acpi_device(dev->parent), &index); if (ret < 0) return ret; ret = dell_wmi_ddv_query_string(data->wdev, DELL_DDV_BATTERY_EPPID, index, &obj); if (ret < 0) return ret; if (obj->string.length != DELL_EPPID_LENGTH && obj->string.length != DELL_EPPID_EXT_LENGTH) dev_info_once(&data->wdev->dev, FW_INFO "Suspicious ePPID length (%d)\n", obj->string.length); ret = sysfs_emit(buf, "%s\n", obj->string.pointer); kfree(obj); return ret; } static int dell_wmi_ddv_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) { struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook); u32 index; int ret; /* Return 0 instead of error to avoid being unloaded */ ret = dell_wmi_ddv_battery_index(to_acpi_device(battery->dev.parent), &index); if (ret < 0) return 0; ret = device_create_file(&battery->dev, &data->temp_attr); if (ret < 0) return ret; ret = device_create_file(&battery->dev, &data->eppid_attr); if (ret < 0) { device_remove_file(&battery->dev, &data->temp_attr); return ret; } return 0; } static int dell_wmi_ddv_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) { struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook); device_remove_file(&battery->dev, &data->temp_attr); device_remove_file(&battery->dev, &data->eppid_attr); return 0; } static void dell_wmi_ddv_battery_remove(void *data) { struct acpi_battery_hook *hook = data; battery_hook_unregister(hook); } static int dell_wmi_ddv_battery_add(struct dell_wmi_ddv_data *data) { data->hook.name = "Dell DDV Battery Extension"; data->hook.add_battery = dell_wmi_ddv_add_battery; data->hook.remove_battery = dell_wmi_ddv_remove_battery; sysfs_attr_init(&data->temp_attr.attr); data->temp_attr.attr.name = "temp"; data->temp_attr.attr.mode = 0444; data->temp_attr.show = temp_show; sysfs_attr_init(&data->eppid_attr.attr); data->eppid_attr.attr.name = "eppid"; data->eppid_attr.attr.mode = 0444; data->eppid_attr.show = eppid_show; battery_hook_register(&data->hook); return devm_add_action_or_reset(&data->wdev->dev, dell_wmi_ddv_battery_remove, &data->hook); } static int dell_wmi_ddv_buffer_read(struct seq_file *seq, enum dell_ddv_method method) { struct device *dev = seq->private; struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); union acpi_object *obj; u64 size; u8 *buf; int ret; ret = dell_wmi_ddv_query_buffer(data->wdev, method, 0, &obj); if (ret < 0) return ret; size = obj->package.elements[0].integer.value; buf = obj->package.elements[1].buffer.pointer; ret = seq_write(seq, buf, size); kfree(obj); return ret; } static int dell_wmi_ddv_fan_read(struct seq_file *seq, void *offset) { return dell_wmi_ddv_buffer_read(seq, DELL_DDV_FAN_SENSOR_INFORMATION); } static int dell_wmi_ddv_temp_read(struct seq_file *seq, void *offset) { return dell_wmi_ddv_buffer_read(seq, DELL_DDV_THERMAL_SENSOR_INFORMATION); } static void dell_wmi_ddv_debugfs_remove(void *data) { struct dentry *entry = data; debugfs_remove(entry); } static void dell_wmi_ddv_debugfs_init(struct wmi_device *wdev) { struct dentry *entry; char name[64]; scnprintf(name, ARRAY_SIZE(name), "%s-%s", DRIVER_NAME, dev_name(&wdev->dev)); entry = debugfs_create_dir(name, NULL); debugfs_create_devm_seqfile(&wdev->dev, "fan_sensor_information", entry, dell_wmi_ddv_fan_read); debugfs_create_devm_seqfile(&wdev->dev, "thermal_sensor_information", entry, dell_wmi_ddv_temp_read); devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_debugfs_remove, entry); } static int dell_wmi_ddv_probe(struct wmi_device *wdev, const void *context) { struct dell_wmi_ddv_data *data; u32 version; int ret; ret = dell_wmi_ddv_query_integer(wdev, DELL_DDV_INTERFACE_VERSION, 0, &version); if (ret < 0) return ret; dev_dbg(&wdev->dev, "WMI interface version: %d\n", version); if (version < DELL_DDV_SUPPORTED_VERSION_MIN || version > DELL_DDV_SUPPORTED_VERSION_MAX) { if (!force) return -ENODEV; dev_warn(&wdev->dev, "Loading despite unsupported WMI interface version (%u)\n", version); } data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; dev_set_drvdata(&wdev->dev, data); data->wdev = wdev; dell_wmi_ddv_debugfs_init(wdev); if (IS_REACHABLE(CONFIG_ACPI_BATTERY)) { ret = dell_wmi_ddv_battery_add(data); if (ret < 0 && ret != -ENODEV) dev_warn(&wdev->dev, "Unable to register ACPI battery hook: %d\n", ret); } if (IS_REACHABLE(CONFIG_HWMON)) { ret = dell_wmi_ddv_hwmon_add(data); if (ret < 0 && ret != -ENODEV) dev_warn(&wdev->dev, "Unable to register hwmon interface: %d\n", ret); } return 0; } static int dell_wmi_ddv_resume(struct device *dev) { struct dell_wmi_ddv_data *data = dev_get_drvdata(dev); /* Force re-reading of all active sensors */ dell_wmi_ddv_hwmon_cache_invalidate(&data->fans); dell_wmi_ddv_hwmon_cache_invalidate(&data->temps); return 0; } static DEFINE_SIMPLE_DEV_PM_OPS(dell_wmi_ddv_dev_pm_ops, NULL, dell_wmi_ddv_resume); static const struct wmi_device_id dell_wmi_ddv_id_table[] = { { DELL_DDV_GUID, NULL }, { } }; MODULE_DEVICE_TABLE(wmi, dell_wmi_ddv_id_table); static struct wmi_driver dell_wmi_ddv_driver = { .driver = { .name = DRIVER_NAME, .probe_type = PROBE_PREFER_ASYNCHRONOUS, .pm = pm_sleep_ptr(&dell_wmi_ddv_dev_pm_ops), }, .id_table = dell_wmi_ddv_id_table, .probe = dell_wmi_ddv_probe, }; module_wmi_driver(dell_wmi_ddv_driver); MODULE_AUTHOR("Armin Wolf "); MODULE_DESCRIPTION("Dell WMI sensor driver"); MODULE_LICENSE("GPL");