| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * processor thermal device interface for reading workload type hints |
| * from the user space. The hints are provided by the firmware. |
| * |
| * Operation: |
| * When user space enables workload type prediction: |
| * - Use mailbox to: |
| * Configure notification delay |
| * Enable processor thermal device interrupt |
| * |
| * - The predicted workload type can be read from MMIO: |
| * Offset 0x5B18 shows if there was an interrupt |
| * active for change in workload type and also |
| * predicted workload type. |
| * |
| * Two interface functions are provided to call when there is a |
| * thermal device interrupt: |
| * - proc_thermal_check_wt_intr(): |
| * Check if the interrupt is for change in workload type. Called from |
| * interrupt context. |
| * |
| * - proc_thermal_wt_intr_callback(): |
| * Callback for interrupt processing in thread context. This involves |
| * sending notification to user space that there is a change in the |
| * workload type. |
| * |
| * Copyright (c) 2023, Intel Corporation. |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/pci.h> |
| #include "processor_thermal_device.h" |
| |
| #define SOC_WT GENMASK_ULL(47, 40) |
| |
| #define SOC_WT_PREDICTION_INT_ENABLE_BIT 23 |
| |
| #define SOC_WT_PREDICTION_INT_ACTIVE BIT(2) |
| |
| /* |
| * Closest possible to 1 Second is 1024 ms with programmed time delay |
| * of 0x0A. |
| */ |
| static u8 notify_delay = 0x0A; |
| static u16 notify_delay_ms = 1024; |
| |
| static DEFINE_MUTEX(wt_lock); |
| static u8 wt_enable; |
| |
| /* Show current predicted workload type index */ |
| static ssize_t workload_type_index_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct proc_thermal_device *proc_priv; |
| struct pci_dev *pdev = to_pci_dev(dev); |
| u64 status = 0; |
| int wt; |
| |
| mutex_lock(&wt_lock); |
| if (!wt_enable) { |
| mutex_unlock(&wt_lock); |
| return -ENODATA; |
| } |
| |
| proc_priv = pci_get_drvdata(pdev); |
| |
| status = readq(proc_priv->mmio_base + SOC_WT_RES_INT_STATUS_OFFSET); |
| |
| mutex_unlock(&wt_lock); |
| |
| wt = FIELD_GET(SOC_WT, status); |
| |
| return sysfs_emit(buf, "%d\n", wt); |
| } |
| |
| static DEVICE_ATTR_RO(workload_type_index); |
| |
| static ssize_t workload_hint_enable_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return sysfs_emit(buf, "%d\n", wt_enable); |
| } |
| |
| static ssize_t workload_hint_enable_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct pci_dev *pdev = to_pci_dev(dev); |
| u8 mode; |
| int ret; |
| |
| if (kstrtou8(buf, 10, &mode) || mode > 1) |
| return -EINVAL; |
| |
| mutex_lock(&wt_lock); |
| |
| if (mode) |
| ret = processor_thermal_mbox_interrupt_config(pdev, true, |
| SOC_WT_PREDICTION_INT_ENABLE_BIT, |
| notify_delay); |
| else |
| ret = processor_thermal_mbox_interrupt_config(pdev, false, |
| SOC_WT_PREDICTION_INT_ENABLE_BIT, 0); |
| |
| if (ret) |
| goto ret_enable_store; |
| |
| ret = size; |
| wt_enable = mode; |
| |
| ret_enable_store: |
| mutex_unlock(&wt_lock); |
| |
| return ret; |
| } |
| |
| static DEVICE_ATTR_RW(workload_hint_enable); |
| |
| static ssize_t notification_delay_ms_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| return sysfs_emit(buf, "%u\n", notify_delay_ms); |
| } |
| |
| static ssize_t notification_delay_ms_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t size) |
| { |
| struct pci_dev *pdev = to_pci_dev(dev); |
| u16 new_tw; |
| int ret; |
| u8 tm; |
| |
| /* |
| * Time window register value: |
| * Formula: (1 + x/4) * power(2,y) |
| * x = 2 msbs, that is [30:29] y = 5 [28:24] |
| * in INTR_CONFIG register. |
| * The result will be in milli seconds. |
| * Here, just keep x = 0, and just change y. |
| * First round up the user value to power of 2 and |
| * then take log2, to get "y" value to program. |
| */ |
| ret = kstrtou16(buf, 10, &new_tw); |
| if (ret) |
| return ret; |
| |
| if (!new_tw) |
| return -EINVAL; |
| |
| new_tw = roundup_pow_of_two(new_tw); |
| tm = ilog2(new_tw); |
| if (tm > 31) |
| return -EINVAL; |
| |
| mutex_lock(&wt_lock); |
| |
| /* If the workload hint was already enabled, then update with the new delay */ |
| if (wt_enable) |
| ret = processor_thermal_mbox_interrupt_config(pdev, true, |
| SOC_WT_PREDICTION_INT_ENABLE_BIT, |
| tm); |
| |
| if (!ret) { |
| ret = size; |
| notify_delay = tm; |
| notify_delay_ms = new_tw; |
| } |
| |
| mutex_unlock(&wt_lock); |
| |
| return ret; |
| } |
| |
| static DEVICE_ATTR_RW(notification_delay_ms); |
| |
| static struct attribute *workload_hint_attrs[] = { |
| &dev_attr_workload_type_index.attr, |
| &dev_attr_workload_hint_enable.attr, |
| &dev_attr_notification_delay_ms.attr, |
| NULL |
| }; |
| |
| static const struct attribute_group workload_hint_attribute_group = { |
| .attrs = workload_hint_attrs, |
| .name = "workload_hint" |
| }; |
| |
| /* |
| * Callback to check if the interrupt for prediction is active. |
| * Caution: Called from the interrupt context. |
| */ |
| bool proc_thermal_check_wt_intr(struct proc_thermal_device *proc_priv) |
| { |
| u64 int_status; |
| |
| int_status = readq(proc_priv->mmio_base + SOC_WT_RES_INT_STATUS_OFFSET); |
| if (int_status & SOC_WT_PREDICTION_INT_ACTIVE) |
| return true; |
| |
| return false; |
| } |
| EXPORT_SYMBOL_NS_GPL(proc_thermal_check_wt_intr, INT340X_THERMAL); |
| |
| /* Callback to notify user space */ |
| void proc_thermal_wt_intr_callback(struct pci_dev *pdev, struct proc_thermal_device *proc_priv) |
| { |
| u64 status; |
| |
| status = readq(proc_priv->mmio_base + SOC_WT_RES_INT_STATUS_OFFSET); |
| if (!(status & SOC_WT_PREDICTION_INT_ACTIVE)) |
| return; |
| |
| sysfs_notify(&pdev->dev.kobj, "workload_hint", "workload_type_index"); |
| } |
| EXPORT_SYMBOL_NS_GPL(proc_thermal_wt_intr_callback, INT340X_THERMAL); |
| |
| static bool workload_hint_created; |
| |
| int proc_thermal_wt_hint_add(struct pci_dev *pdev, struct proc_thermal_device *proc_priv) |
| { |
| int ret; |
| |
| ret = sysfs_create_group(&pdev->dev.kobj, &workload_hint_attribute_group); |
| if (ret) |
| return ret; |
| |
| workload_hint_created = true; |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_NS_GPL(proc_thermal_wt_hint_add, INT340X_THERMAL); |
| |
| void proc_thermal_wt_hint_remove(struct pci_dev *pdev) |
| { |
| mutex_lock(&wt_lock); |
| if (wt_enable) |
| processor_thermal_mbox_interrupt_config(pdev, false, |
| SOC_WT_PREDICTION_INT_ENABLE_BIT, |
| 0); |
| mutex_unlock(&wt_lock); |
| |
| if (workload_hint_created) |
| sysfs_remove_group(&pdev->dev.kobj, &workload_hint_attribute_group); |
| |
| workload_hint_created = false; |
| } |
| EXPORT_SYMBOL_NS_GPL(proc_thermal_wt_hint_remove, INT340X_THERMAL); |
| |
| MODULE_IMPORT_NS(INT340X_THERMAL); |
| MODULE_LICENSE("GPL"); |