| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * AMD Promontory 21 xHCI Hwmon Implementation | |
| * (only temperature monitoring is supported) | |
| * | |
| * This can be effectively used as the alternative chipset temperature monitor. | |
| * | |
| * Copyright (C) 2026 Jihong Min <hurryman2212@gmail.com> | |
| */ | |
| #include <linux/auxiliary_bus.h> | |
| #include <linux/device.h> | |
| #include <linux/err.h> | |
| #include <linux/errno.h> | |
| #include <linux/hwmon.h> | |
| #include <linux/io.h> | |
| #include <linux/math.h> | |
| #include <linux/module.h> | |
| #include <linux/pci.h> | |
| #include <linux/platform_data/usb-xhci-prom21.h> | |
| #include <linux/pm_runtime.h> | |
| #define PROM21_XHCI_INDEX_OFFSET 0x3000 | |
| #define PROM21_XHCI_DATA_OFFSET 0x3008 | |
| #define PROM21_XHCI_TEMP_SELECTOR 0x0001e520 | |
| struct prom21_xhci { | |
| struct pci_dev *pdev; | |
| struct device *hwmon_dev; | |
| void __iomem *regs; | |
| }; | |
| static int prom21_xhci_pm_get(struct prom21_xhci *hwmon) | |
| { | |
| struct device *dev = &hwmon->pdev->dev; | |
| int ret; | |
| /* | |
| * PROM21 temperature register access does not return a valid value while | |
| * the parent xHCI PCI function is suspended. Do not wake the device from | |
| * a hwmon read. On success, hold a usage reference without changing the | |
| * runtime PM state; if runtime PM is disabled, allow the read unless the | |
| * device is still marked suspended. | |
| */ | |
| ret = pm_runtime_get_if_active(dev); | |
| if (ret > 0) | |
| return 0; | |
| if (ret == -EINVAL) { | |
| if (pm_runtime_status_suspended(dev)) | |
| return -ENODATA; | |
| pm_runtime_get_noresume(dev); | |
| return 0; | |
| } | |
| if (!ret) | |
| return -ENODATA; | |
| return ret; | |
| } | |
| /* | |
| * This is not a pure MMIO read. The PROM21 vendor data register is selected | |
| * by temporarily writing PROM21_XHCI_TEMP_SELECTOR to the vendor index | |
| * register. | |
| * The hwmon core already serializes this driver's callbacks, so this driver | |
| * does not need an additional private lock. That does not synchronize with | |
| * firmware, SMM, ACPI, or other possible users. Keep the sequence short and | |
| * restore the previous index before returning. | |
| */ | |
| static int prom21_xhci_read_temp_raw_restore_index(struct prom21_xhci *hwmon, | |
| u8 *raw) | |
| { | |
| struct device *dev = &hwmon->pdev->dev; | |
| u32 index; | |
| u8 data; | |
| int ret; | |
| ret = prom21_xhci_pm_get(hwmon); | |
| if (ret) | |
| return ret; | |
| index = readl(hwmon->regs + PROM21_XHCI_INDEX_OFFSET); | |
| /* Select the PROM21 temperature register through the vendor index. */ | |
| writel(PROM21_XHCI_TEMP_SELECTOR, | |
| hwmon->regs + PROM21_XHCI_INDEX_OFFSET); | |
| /* Use a 32-bit read for PCI MMIO register access. */ | |
| data = readl(hwmon->regs + PROM21_XHCI_DATA_OFFSET) & 0xff; | |
| /* Restore the previous vendor index register value. */ | |
| writel(index, hwmon->regs + PROM21_XHCI_INDEX_OFFSET); | |
| readl(hwmon->regs + PROM21_XHCI_INDEX_OFFSET); | |
| /* | |
| * Drop the usage reference taken by prom21_xhci_pm_get(). This is | |
| * enough because the read path never resumes the device; use the normal | |
| * put path so the PM core can re-evaluate idle state after the read. | |
| * Otherwise, a racing xHCI autosuspend attempt can see a nonzero | |
| * runtime PM usage count and skip autosuspend, and a later | |
| * pm_runtime_put_noidle(), which does not check for an idle device, | |
| * would leave the device active. | |
| */ | |
| pm_runtime_put(dev); | |
| if (!data) | |
| return -ENODATA; | |
| *raw = data; | |
| return 0; | |
| } | |
| static long prom21_xhci_raw_to_millicelsius(u8 raw) | |
| { | |
| /* | |
| * No public AMD reference is available for this value. | |
| * The scale was derived from observed PROM21 xHCI temperature readings: | |
| * temp[C] = raw * 0.9066 - 78.624 | |
| */ | |
| return DIV_ROUND_CLOSEST(raw * 9066, 10) - 78624; | |
| } | |
| static umode_t prom21_xhci_is_visible(const void *drvdata, | |
| enum hwmon_sensor_types type, u32 attr, | |
| int channel) | |
| { | |
| if (type != hwmon_temp) | |
| return 0; | |
| switch (attr) { | |
| case hwmon_temp_input: | |
| return 0444; | |
| default: | |
| return 0; | |
| } | |
| } | |
| static int prom21_xhci_read(struct device *dev, enum hwmon_sensor_types type, | |
| u32 attr, int channel, long *val) | |
| { | |
| struct prom21_xhci *hwmon = dev_get_drvdata(dev); | |
| u8 raw; | |
| int ret; | |
| if (type != hwmon_temp || attr != hwmon_temp_input) | |
| return -EOPNOTSUPP; | |
| ret = prom21_xhci_read_temp_raw_restore_index(hwmon, &raw); | |
| if (ret) | |
| return ret; | |
| *val = prom21_xhci_raw_to_millicelsius(raw); | |
| return 0; | |
| } | |
| static const struct hwmon_ops prom21_xhci_ops = { | |
| .is_visible = prom21_xhci_is_visible, | |
| .read = prom21_xhci_read, | |
| }; | |
| static const struct hwmon_channel_info *const prom21_xhci_info[] = { | |
| HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), | |
| NULL, | |
| }; | |
| static const struct hwmon_chip_info prom21_xhci_chip_info = { | |
| .ops = &prom21_xhci_ops, | |
| .info = prom21_xhci_info, | |
| }; | |
| static int prom21_xhci_probe(struct auxiliary_device *auxdev, | |
| const struct auxiliary_device_id *id) | |
| { | |
| struct device *dev = &auxdev->dev; | |
| const struct prom21_xhci_pdata *pdata = dev_get_platdata(dev); | |
| struct prom21_xhci *hwmon; | |
| if (!pdata) | |
| return dev_err_probe(dev, -ENODEV, | |
| "platform data unavailable\n"); | |
| if (!pdata->regs || | |
| pdata->rsrc_len < PROM21_XHCI_DATA_OFFSET + sizeof(u32)) | |
| return dev_err_probe(dev, -ENODEV, "invalid MMIO resource\n"); | |
| hwmon = devm_kzalloc(dev, sizeof(*hwmon), GFP_KERNEL); | |
| if (!hwmon) | |
| return -ENOMEM; | |
| hwmon->pdev = pdata->pdev; | |
| hwmon->regs = pdata->regs; | |
| auxiliary_set_drvdata(auxdev, hwmon); | |
| /* | |
| * Parent the hwmon device to the PCI function because the temperature | |
| * value is read from that function's MMIO BAR, and systems may contain | |
| * multiple PROM21 xHCI functions. This lets userspace identify the PCI | |
| * endpoint for each reading. The auxiliary driver still owns the hwmon | |
| * lifetime and unregisters it before HCD teardown. | |
| */ | |
| hwmon->hwmon_dev = | |
| hwmon_device_register_with_info(&pdata->pdev->dev, "prom21_xhci", | |
| hwmon, &prom21_xhci_chip_info, | |
| NULL); | |
| if (IS_ERR(hwmon->hwmon_dev)) | |
| return PTR_ERR(hwmon->hwmon_dev); | |
| return 0; | |
| } | |
| static void prom21_xhci_remove(struct auxiliary_device *auxdev) | |
| { | |
| struct prom21_xhci *hwmon = auxiliary_get_drvdata(auxdev); | |
| /* | |
| * The PROM21 PCI glue destroys the auxiliary device before HCD teardown. | |
| * Unregister the hwmon device here so sysfs removes the attributes, | |
| * stops new reads, and drains active hwmon callbacks before the xHCI | |
| * MMIO mapping is released. | |
| */ | |
| hwmon_device_unregister(hwmon->hwmon_dev); | |
| } | |
| static const struct auxiliary_device_id prom21_xhci_id_table[] = { | |
| { .name = "xhci_pci_prom21.hwmon" }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(auxiliary, prom21_xhci_id_table); | |
| static struct auxiliary_driver prom21_xhci_driver = { | |
| .name = "prom21-xhci", | |
| .probe = prom21_xhci_probe, | |
| .remove = prom21_xhci_remove, | |
| .id_table = prom21_xhci_id_table, | |
| }; | |
| module_auxiliary_driver(prom21_xhci_driver); | |
| MODULE_AUTHOR("Jihong Min <hurryman2212@gmail.com>"); | |
| MODULE_DESCRIPTION("AMD Promontory 21 xHCI temperature sensor driver"); | |
| MODULE_LICENSE("GPL"); |