// 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 */ #include #include #include #include #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("AMD Promontory 21 xHCI temperature sensor driver"); MODULE_LICENSE("GPL");