1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
// 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");
|