| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * INT3400 thermal driver |
| * |
| * Copyright (C) 2014, Intel Corporation |
| * Authors: Zhang Rui <rui.zhang@intel.com> |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/acpi.h> |
| #include <linux/thermal.h> |
| #include "acpi_thermal_rel.h" |
| |
| #define INT3400_THERMAL_TABLE_CHANGED 0x83 |
| #define INT3400_ODVP_CHANGED 0x88 |
| #define INT3400_KEEP_ALIVE 0xA0 |
| |
| enum int3400_thermal_uuid { |
| INT3400_THERMAL_PASSIVE_1, |
| INT3400_THERMAL_ACTIVE, |
| INT3400_THERMAL_CRITICAL, |
| INT3400_THERMAL_ADAPTIVE_PERFORMANCE, |
| INT3400_THERMAL_EMERGENCY_CALL_MODE, |
| INT3400_THERMAL_PASSIVE_2, |
| INT3400_THERMAL_POWER_BOSS, |
| INT3400_THERMAL_VIRTUAL_SENSOR, |
| INT3400_THERMAL_COOLING_MODE, |
| INT3400_THERMAL_HARDWARE_DUTY_CYCLING, |
| INT3400_THERMAL_MAXIMUM_UUID, |
| }; |
| |
| static char *int3400_thermal_uuids[INT3400_THERMAL_MAXIMUM_UUID] = { |
| "42A441D6-AE6A-462b-A84B-4A8CE79027D3", |
| "3A95C389-E4B8-4629-A526-C52C88626BAE", |
| "97C68AE7-15FA-499c-B8C9-5DA81D606E0A", |
| "63BE270F-1C11-48FD-A6F7-3AF253FF3E2D", |
| "5349962F-71E6-431D-9AE8-0A635B710AEE", |
| "9E04115A-AE87-4D1C-9500-0F3E340BFE75", |
| "F5A35014-C209-46A4-993A-EB56DE7530A1", |
| "6ED722A7-9240-48A5-B479-31EEF723D7CF", |
| "16CAF1B7-DD38-40ED-B1C1-1B8A1913D531", |
| "BE84BABF-C4D4-403D-B495-3128FD44dAC1", |
| }; |
| |
| struct odvp_attr; |
| |
| struct int3400_thermal_priv { |
| struct acpi_device *adev; |
| struct platform_device *pdev; |
| struct thermal_zone_device *thermal; |
| int art_count; |
| struct art *arts; |
| int trt_count; |
| struct trt *trts; |
| u8 uuid_bitmap; |
| int rel_misc_dev_res; |
| int current_uuid_index; |
| char *data_vault; |
| int odvp_count; |
| int *odvp; |
| struct odvp_attr *odvp_attrs; |
| }; |
| |
| static int evaluate_odvp(struct int3400_thermal_priv *priv); |
| |
| struct odvp_attr { |
| int odvp; |
| struct int3400_thermal_priv *priv; |
| struct kobj_attribute attr; |
| }; |
| |
| static ssize_t data_vault_read(struct file *file, struct kobject *kobj, |
| struct bin_attribute *attr, char *buf, loff_t off, size_t count) |
| { |
| memcpy(buf, attr->private + off, count); |
| return count; |
| } |
| |
| static BIN_ATTR_RO(data_vault, 0); |
| |
| static struct bin_attribute *data_attributes[] = { |
| &bin_attr_data_vault, |
| NULL, |
| }; |
| |
| static ssize_t imok_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct int3400_thermal_priv *priv = dev_get_drvdata(dev); |
| acpi_status status; |
| int input, ret; |
| |
| ret = kstrtouint(buf, 10, &input); |
| if (ret) |
| return ret; |
| status = acpi_execute_simple_method(priv->adev->handle, "IMOK", input); |
| if (ACPI_FAILURE(status)) |
| return -EIO; |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_WO(imok); |
| |
| static struct attribute *imok_attr[] = { |
| &dev_attr_imok.attr, |
| NULL |
| }; |
| |
| static const struct attribute_group imok_attribute_group = { |
| .attrs = imok_attr, |
| }; |
| |
| static const struct attribute_group data_attribute_group = { |
| .bin_attrs = data_attributes, |
| }; |
| |
| static ssize_t available_uuids_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct int3400_thermal_priv *priv = dev_get_drvdata(dev); |
| int i; |
| int length = 0; |
| |
| if (!priv->uuid_bitmap) |
| return sprintf(buf, "UNKNOWN\n"); |
| |
| for (i = 0; i < INT3400_THERMAL_MAXIMUM_UUID; i++) { |
| if (priv->uuid_bitmap & (1 << i)) |
| length += scnprintf(&buf[length], |
| PAGE_SIZE - length, |
| "%s\n", |
| int3400_thermal_uuids[i]); |
| } |
| |
| return length; |
| } |
| |
| static ssize_t current_uuid_show(struct device *dev, |
| struct device_attribute *devattr, char *buf) |
| { |
| struct int3400_thermal_priv *priv = dev_get_drvdata(dev); |
| |
| if (priv->current_uuid_index == -1) |
| return sprintf(buf, "INVALID\n"); |
| |
| return sprintf(buf, "%s\n", |
| int3400_thermal_uuids[priv->current_uuid_index]); |
| } |
| |
| static ssize_t current_uuid_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct int3400_thermal_priv *priv = dev_get_drvdata(dev); |
| int i; |
| |
| for (i = 0; i < INT3400_THERMAL_MAXIMUM_UUID; ++i) { |
| if (!strncmp(buf, int3400_thermal_uuids[i], |
| sizeof(int3400_thermal_uuids[i]) - 1)) { |
| /* |
| * If we have a list of supported UUIDs, make sure |
| * this one is supported. |
| */ |
| if (priv->uuid_bitmap && |
| !(priv->uuid_bitmap & (1 << i))) |
| return -EINVAL; |
| |
| priv->current_uuid_index = i; |
| return count; |
| } |
| } |
| |
| return -EINVAL; |
| } |
| |
| static DEVICE_ATTR_RW(current_uuid); |
| static DEVICE_ATTR_RO(available_uuids); |
| static struct attribute *uuid_attrs[] = { |
| &dev_attr_available_uuids.attr, |
| &dev_attr_current_uuid.attr, |
| NULL |
| }; |
| |
| static const struct attribute_group uuid_attribute_group = { |
| .attrs = uuid_attrs, |
| .name = "uuids" |
| }; |
| |
| static int int3400_thermal_get_uuids(struct int3400_thermal_priv *priv) |
| { |
| struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL}; |
| union acpi_object *obja, *objb; |
| int i, j; |
| int result = 0; |
| acpi_status status; |
| |
| status = acpi_evaluate_object(priv->adev->handle, "IDSP", NULL, &buf); |
| if (ACPI_FAILURE(status)) |
| return -ENODEV; |
| |
| obja = (union acpi_object *)buf.pointer; |
| if (obja->type != ACPI_TYPE_PACKAGE) { |
| result = -EINVAL; |
| goto end; |
| } |
| |
| for (i = 0; i < obja->package.count; i++) { |
| objb = &obja->package.elements[i]; |
| if (objb->type != ACPI_TYPE_BUFFER) { |
| result = -EINVAL; |
| goto end; |
| } |
| |
| /* UUID must be 16 bytes */ |
| if (objb->buffer.length != 16) { |
| result = -EINVAL; |
| goto end; |
| } |
| |
| for (j = 0; j < INT3400_THERMAL_MAXIMUM_UUID; j++) { |
| guid_t guid; |
| |
| guid_parse(int3400_thermal_uuids[j], &guid); |
| if (guid_equal((guid_t *)objb->buffer.pointer, &guid)) { |
| priv->uuid_bitmap |= (1 << j); |
| break; |
| } |
| } |
| } |
| |
| end: |
| kfree(buf.pointer); |
| return result; |
| } |
| |
| static int int3400_thermal_run_osc(acpi_handle handle, |
| enum int3400_thermal_uuid uuid, bool enable) |
| { |
| u32 ret, buf[2]; |
| acpi_status status; |
| int result = 0; |
| struct acpi_osc_context context = { |
| .uuid_str = NULL, |
| .rev = 1, |
| .cap.length = 8, |
| }; |
| |
| if (uuid < 0 || uuid >= INT3400_THERMAL_MAXIMUM_UUID) |
| return -EINVAL; |
| |
| context.uuid_str = int3400_thermal_uuids[uuid]; |
| |
| buf[OSC_QUERY_DWORD] = 0; |
| buf[OSC_SUPPORT_DWORD] = enable; |
| |
| context.cap.pointer = buf; |
| |
| status = acpi_run_osc(handle, &context); |
| if (ACPI_SUCCESS(status)) { |
| ret = *((u32 *)(context.ret.pointer + 4)); |
| if (ret != enable) |
| result = -EPERM; |
| } else |
| result = -EPERM; |
| |
| kfree(context.ret.pointer); |
| |
| return result; |
| } |
| |
| static ssize_t odvp_show(struct kobject *kobj, struct kobj_attribute *attr, |
| char *buf) |
| { |
| struct odvp_attr *odvp_attr; |
| |
| odvp_attr = container_of(attr, struct odvp_attr, attr); |
| |
| return sprintf(buf, "%d\n", odvp_attr->priv->odvp[odvp_attr->odvp]); |
| } |
| |
| static void cleanup_odvp(struct int3400_thermal_priv *priv) |
| { |
| int i; |
| |
| if (priv->odvp_attrs) { |
| for (i = 0; i < priv->odvp_count; i++) { |
| sysfs_remove_file(&priv->pdev->dev.kobj, |
| &priv->odvp_attrs[i].attr.attr); |
| kfree(priv->odvp_attrs[i].attr.attr.name); |
| } |
| kfree(priv->odvp_attrs); |
| } |
| kfree(priv->odvp); |
| priv->odvp_count = 0; |
| } |
| |
| static int evaluate_odvp(struct int3400_thermal_priv *priv) |
| { |
| struct acpi_buffer odvp = { ACPI_ALLOCATE_BUFFER, NULL }; |
| union acpi_object *obj = NULL; |
| acpi_status status; |
| int i, ret; |
| |
| status = acpi_evaluate_object(priv->adev->handle, "ODVP", NULL, &odvp); |
| if (ACPI_FAILURE(status)) { |
| ret = -EINVAL; |
| goto out_err; |
| } |
| |
| obj = odvp.pointer; |
| if (obj->type != ACPI_TYPE_PACKAGE) { |
| ret = -EINVAL; |
| goto out_err; |
| } |
| |
| if (priv->odvp == NULL) { |
| priv->odvp_count = obj->package.count; |
| priv->odvp = kmalloc_array(priv->odvp_count, sizeof(int), |
| GFP_KERNEL); |
| if (!priv->odvp) { |
| ret = -ENOMEM; |
| goto out_err; |
| } |
| } |
| |
| if (priv->odvp_attrs == NULL) { |
| priv->odvp_attrs = kcalloc(priv->odvp_count, |
| sizeof(struct odvp_attr), |
| GFP_KERNEL); |
| if (!priv->odvp_attrs) { |
| ret = -ENOMEM; |
| goto out_err; |
| } |
| for (i = 0; i < priv->odvp_count; i++) { |
| struct odvp_attr *odvp = &priv->odvp_attrs[i]; |
| |
| sysfs_attr_init(&odvp->attr.attr); |
| odvp->priv = priv; |
| odvp->odvp = i; |
| odvp->attr.attr.name = kasprintf(GFP_KERNEL, |
| "odvp%d", i); |
| |
| if (!odvp->attr.attr.name) { |
| ret = -ENOMEM; |
| goto out_err; |
| } |
| odvp->attr.attr.mode = 0444; |
| odvp->attr.show = odvp_show; |
| odvp->attr.store = NULL; |
| ret = sysfs_create_file(&priv->pdev->dev.kobj, |
| &odvp->attr.attr); |
| if (ret) |
| goto out_err; |
| } |
| } |
| |
| for (i = 0; i < obj->package.count; i++) { |
| if (obj->package.elements[i].type == ACPI_TYPE_INTEGER) |
| priv->odvp[i] = obj->package.elements[i].integer.value; |
| } |
| |
| kfree(obj); |
| return 0; |
| |
| out_err: |
| cleanup_odvp(priv); |
| kfree(obj); |
| return ret; |
| } |
| |
| static void int3400_notify(acpi_handle handle, |
| u32 event, |
| void *data) |
| { |
| struct int3400_thermal_priv *priv = data; |
| char *thermal_prop[5]; |
| int therm_event; |
| |
| if (!priv) |
| return; |
| |
| switch (event) { |
| case INT3400_THERMAL_TABLE_CHANGED: |
| therm_event = THERMAL_TABLE_CHANGED; |
| break; |
| case INT3400_KEEP_ALIVE: |
| therm_event = THERMAL_EVENT_KEEP_ALIVE; |
| break; |
| case INT3400_ODVP_CHANGED: |
| evaluate_odvp(priv); |
| therm_event = THERMAL_DEVICE_POWER_CAPABILITY_CHANGED; |
| break; |
| default: |
| /* Ignore unknown notification codes sent to INT3400 device */ |
| return; |
| } |
| |
| thermal_prop[0] = kasprintf(GFP_KERNEL, "NAME=%s", priv->thermal->type); |
| thermal_prop[1] = kasprintf(GFP_KERNEL, "TEMP=%d", priv->thermal->temperature); |
| thermal_prop[2] = kasprintf(GFP_KERNEL, "TRIP="); |
| thermal_prop[3] = kasprintf(GFP_KERNEL, "EVENT=%d", therm_event); |
| thermal_prop[4] = NULL; |
| kobject_uevent_env(&priv->thermal->device.kobj, KOBJ_CHANGE, thermal_prop); |
| } |
| |
| static int int3400_thermal_get_temp(struct thermal_zone_device *thermal, |
| int *temp) |
| { |
| *temp = 20 * 1000; /* faked temp sensor with 20C */ |
| return 0; |
| } |
| |
| static int int3400_thermal_change_mode(struct thermal_zone_device *thermal, |
| enum thermal_device_mode mode) |
| { |
| struct int3400_thermal_priv *priv = thermal->devdata; |
| int result = 0; |
| |
| if (!priv) |
| return -EINVAL; |
| |
| if (mode != thermal->mode) |
| result = int3400_thermal_run_osc(priv->adev->handle, |
| priv->current_uuid_index, |
| mode == THERMAL_DEVICE_ENABLED); |
| |
| |
| evaluate_odvp(priv); |
| |
| return result; |
| } |
| |
| static struct thermal_zone_device_ops int3400_thermal_ops = { |
| .get_temp = int3400_thermal_get_temp, |
| .change_mode = int3400_thermal_change_mode, |
| }; |
| |
| static struct thermal_zone_params int3400_thermal_params = { |
| .governor_name = "user_space", |
| .no_hwmon = true, |
| }; |
| |
| static void int3400_setup_gddv(struct int3400_thermal_priv *priv) |
| { |
| struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; |
| union acpi_object *obj; |
| acpi_status status; |
| |
| status = acpi_evaluate_object(priv->adev->handle, "GDDV", NULL, |
| &buffer); |
| if (ACPI_FAILURE(status) || !buffer.length) |
| return; |
| |
| obj = buffer.pointer; |
| if (obj->type != ACPI_TYPE_PACKAGE || obj->package.count != 1 |
| || obj->package.elements[0].type != ACPI_TYPE_BUFFER) { |
| kfree(buffer.pointer); |
| return; |
| } |
| |
| priv->data_vault = kmemdup(obj->package.elements[0].buffer.pointer, |
| obj->package.elements[0].buffer.length, |
| GFP_KERNEL); |
| bin_attr_data_vault.private = priv->data_vault; |
| bin_attr_data_vault.size = obj->package.elements[0].buffer.length; |
| kfree(buffer.pointer); |
| } |
| |
| static int int3400_thermal_probe(struct platform_device *pdev) |
| { |
| struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); |
| struct int3400_thermal_priv *priv; |
| int result; |
| |
| if (!adev) |
| return -ENODEV; |
| |
| priv = kzalloc(sizeof(struct int3400_thermal_priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| priv->pdev = pdev; |
| priv->adev = adev; |
| |
| result = int3400_thermal_get_uuids(priv); |
| |
| /* Missing IDSP isn't fatal */ |
| if (result && result != -ENODEV) |
| goto free_priv; |
| |
| priv->current_uuid_index = -1; |
| |
| result = acpi_parse_art(priv->adev->handle, &priv->art_count, |
| &priv->arts, true); |
| if (result) |
| dev_dbg(&pdev->dev, "_ART table parsing error\n"); |
| |
| result = acpi_parse_trt(priv->adev->handle, &priv->trt_count, |
| &priv->trts, true); |
| if (result) |
| dev_dbg(&pdev->dev, "_TRT table parsing error\n"); |
| |
| platform_set_drvdata(pdev, priv); |
| |
| int3400_setup_gddv(priv); |
| |
| evaluate_odvp(priv); |
| |
| priv->thermal = thermal_zone_device_register("INT3400 Thermal", 0, 0, |
| priv, &int3400_thermal_ops, |
| &int3400_thermal_params, 0, 0); |
| if (IS_ERR(priv->thermal)) { |
| result = PTR_ERR(priv->thermal); |
| goto free_art_trt; |
| } |
| |
| priv->rel_misc_dev_res = acpi_thermal_rel_misc_device_add( |
| priv->adev->handle); |
| |
| result = sysfs_create_group(&pdev->dev.kobj, &uuid_attribute_group); |
| if (result) |
| goto free_rel_misc; |
| |
| if (acpi_has_method(priv->adev->handle, "IMOK")) { |
| result = sysfs_create_group(&pdev->dev.kobj, &imok_attribute_group); |
| if (result) |
| goto free_imok; |
| } |
| |
| if (priv->data_vault) { |
| result = sysfs_create_group(&pdev->dev.kobj, |
| &data_attribute_group); |
| if (result) |
| goto free_uuid; |
| } |
| |
| result = acpi_install_notify_handler( |
| priv->adev->handle, ACPI_DEVICE_NOTIFY, int3400_notify, |
| (void *)priv); |
| if (result) |
| goto free_sysfs; |
| |
| return 0; |
| |
| free_sysfs: |
| cleanup_odvp(priv); |
| if (priv->data_vault) { |
| sysfs_remove_group(&pdev->dev.kobj, &data_attribute_group); |
| kfree(priv->data_vault); |
| } |
| free_uuid: |
| sysfs_remove_group(&pdev->dev.kobj, &uuid_attribute_group); |
| free_imok: |
| sysfs_remove_group(&pdev->dev.kobj, &imok_attribute_group); |
| free_rel_misc: |
| if (!priv->rel_misc_dev_res) |
| acpi_thermal_rel_misc_device_remove(priv->adev->handle); |
| thermal_zone_device_unregister(priv->thermal); |
| free_art_trt: |
| kfree(priv->trts); |
| kfree(priv->arts); |
| free_priv: |
| kfree(priv); |
| return result; |
| } |
| |
| static int int3400_thermal_remove(struct platform_device *pdev) |
| { |
| struct int3400_thermal_priv *priv = platform_get_drvdata(pdev); |
| |
| acpi_remove_notify_handler( |
| priv->adev->handle, ACPI_DEVICE_NOTIFY, |
| int3400_notify); |
| |
| cleanup_odvp(priv); |
| |
| if (!priv->rel_misc_dev_res) |
| acpi_thermal_rel_misc_device_remove(priv->adev->handle); |
| |
| if (priv->data_vault) |
| sysfs_remove_group(&pdev->dev.kobj, &data_attribute_group); |
| sysfs_remove_group(&pdev->dev.kobj, &uuid_attribute_group); |
| sysfs_remove_group(&pdev->dev.kobj, &imok_attribute_group); |
| thermal_zone_device_unregister(priv->thermal); |
| kfree(priv->data_vault); |
| kfree(priv->trts); |
| kfree(priv->arts); |
| kfree(priv); |
| return 0; |
| } |
| |
| static const struct acpi_device_id int3400_thermal_match[] = { |
| {"INT3400", 0}, |
| {"INTC1040", 0}, |
| {"INTC1041", 0}, |
| {} |
| }; |
| |
| MODULE_DEVICE_TABLE(acpi, int3400_thermal_match); |
| |
| static struct platform_driver int3400_thermal_driver = { |
| .probe = int3400_thermal_probe, |
| .remove = int3400_thermal_remove, |
| .driver = { |
| .name = "int3400 thermal", |
| .acpi_match_table = ACPI_PTR(int3400_thermal_match), |
| }, |
| }; |
| |
| module_platform_driver(int3400_thermal_driver); |
| |
| MODULE_DESCRIPTION("INT3400 Thermal driver"); |
| MODULE_AUTHOR("Zhang Rui <rui.zhang@intel.com>"); |
| MODULE_LICENSE("GPL"); |