1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * A hwmon driver for the Intel 5000 series chipset FB-DIMM AMB |
4 | * temperature sensors |
5 | * Copyright (C) 2007 IBM |
6 | * |
7 | * Author: Darrick J. Wong <darrick.wong@oracle.com> |
8 | */ |
9 | |
10 | #include <linux/module.h> |
11 | #include <linux/hwmon.h> |
12 | #include <linux/hwmon-sysfs.h> |
13 | #include <linux/err.h> |
14 | #include <linux/mutex.h> |
15 | #include <linux/log2.h> |
16 | #include <linux/pci.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/slab.h> |
19 | |
20 | #define DRVNAME "i5k_amb" |
21 | |
22 | #define I5K_REG_AMB_BASE_ADDR 0x48 |
23 | #define I5K_REG_AMB_LEN_ADDR 0x50 |
24 | #define I5K_REG_CHAN0_PRESENCE_ADDR 0x64 |
25 | #define I5K_REG_CHAN1_PRESENCE_ADDR 0x66 |
26 | |
27 | #define AMB_REG_TEMP_MIN_ADDR 0x80 |
28 | #define AMB_REG_TEMP_MID_ADDR 0x81 |
29 | #define AMB_REG_TEMP_MAX_ADDR 0x82 |
30 | #define AMB_REG_TEMP_STATUS_ADDR 0x84 |
31 | #define AMB_REG_TEMP_ADDR 0x85 |
32 | |
33 | #define AMB_CONFIG_SIZE 2048 |
34 | #define AMB_FUNC_3_OFFSET 768 |
35 | |
36 | static unsigned long amb_reg_temp_status(unsigned int amb) |
37 | { |
38 | return AMB_FUNC_3_OFFSET + AMB_REG_TEMP_STATUS_ADDR + |
39 | AMB_CONFIG_SIZE * amb; |
40 | } |
41 | |
42 | static unsigned long amb_reg_temp_min(unsigned int amb) |
43 | { |
44 | return AMB_FUNC_3_OFFSET + AMB_REG_TEMP_MIN_ADDR + |
45 | AMB_CONFIG_SIZE * amb; |
46 | } |
47 | |
48 | static unsigned long amb_reg_temp_mid(unsigned int amb) |
49 | { |
50 | return AMB_FUNC_3_OFFSET + AMB_REG_TEMP_MID_ADDR + |
51 | AMB_CONFIG_SIZE * amb; |
52 | } |
53 | |
54 | static unsigned long amb_reg_temp_max(unsigned int amb) |
55 | { |
56 | return AMB_FUNC_3_OFFSET + AMB_REG_TEMP_MAX_ADDR + |
57 | AMB_CONFIG_SIZE * amb; |
58 | } |
59 | |
60 | static unsigned long amb_reg_temp(unsigned int amb) |
61 | { |
62 | return AMB_FUNC_3_OFFSET + AMB_REG_TEMP_ADDR + |
63 | AMB_CONFIG_SIZE * amb; |
64 | } |
65 | |
66 | #define MAX_MEM_CHANNELS 4 |
67 | #define MAX_AMBS_PER_CHANNEL 16 |
68 | #define MAX_AMBS (MAX_MEM_CHANNELS * \ |
69 | MAX_AMBS_PER_CHANNEL) |
70 | #define CHANNEL_SHIFT 4 |
71 | #define DIMM_MASK 0xF |
72 | /* |
73 | * Ugly hack: For some reason the highest bit is set if there |
74 | * are _any_ DIMMs in the channel. Attempting to read from |
75 | * this "high-order" AMB results in a memory bus error, so |
76 | * for now we'll just ignore that top bit, even though that |
77 | * might prevent us from seeing the 16th DIMM in the channel. |
78 | */ |
79 | #define REAL_MAX_AMBS_PER_CHANNEL 15 |
80 | #define KNOBS_PER_AMB 6 |
81 | |
82 | static unsigned long amb_num_from_reg(unsigned int byte_num, unsigned int bit) |
83 | { |
84 | return byte_num * MAX_AMBS_PER_CHANNEL + bit; |
85 | } |
86 | |
87 | #define AMB_SYSFS_NAME_LEN 16 |
88 | struct i5k_device_attribute { |
89 | struct sensor_device_attribute s_attr; |
90 | char name[AMB_SYSFS_NAME_LEN]; |
91 | }; |
92 | |
93 | struct i5k_amb_data { |
94 | struct device *hwmon_dev; |
95 | |
96 | unsigned long amb_base; |
97 | unsigned long amb_len; |
98 | u16 amb_present[MAX_MEM_CHANNELS]; |
99 | void __iomem *amb_mmio; |
100 | struct i5k_device_attribute *attrs; |
101 | unsigned int num_attrs; |
102 | }; |
103 | |
104 | static ssize_t name_show(struct device *dev, struct device_attribute *devattr, |
105 | char *buf) |
106 | { |
107 | return sprintf(buf, fmt: "%s\n" , DRVNAME); |
108 | } |
109 | |
110 | |
111 | static DEVICE_ATTR_RO(name); |
112 | |
113 | static struct platform_device *amb_pdev; |
114 | |
115 | static u8 amb_read_byte(struct i5k_amb_data *data, unsigned long offset) |
116 | { |
117 | return ioread8(data->amb_mmio + offset); |
118 | } |
119 | |
120 | static void amb_write_byte(struct i5k_amb_data *data, unsigned long offset, |
121 | u8 val) |
122 | { |
123 | iowrite8(val, data->amb_mmio + offset); |
124 | } |
125 | |
126 | static ssize_t show_amb_alarm(struct device *dev, |
127 | struct device_attribute *devattr, |
128 | char *buf) |
129 | { |
130 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
131 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
132 | |
133 | if (!(amb_read_byte(data, offset: amb_reg_temp_status(amb: attr->index)) & 0x20) && |
134 | (amb_read_byte(data, offset: amb_reg_temp_status(amb: attr->index)) & 0x8)) |
135 | return sprintf(buf, fmt: "1\n" ); |
136 | else |
137 | return sprintf(buf, fmt: "0\n" ); |
138 | } |
139 | |
140 | static ssize_t store_amb_min(struct device *dev, |
141 | struct device_attribute *devattr, |
142 | const char *buf, |
143 | size_t count) |
144 | { |
145 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
146 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
147 | unsigned long temp; |
148 | int ret = kstrtoul(s: buf, base: 10, res: &temp); |
149 | if (ret < 0) |
150 | return ret; |
151 | |
152 | temp = temp / 500; |
153 | if (temp > 255) |
154 | temp = 255; |
155 | |
156 | amb_write_byte(data, offset: amb_reg_temp_min(amb: attr->index), val: temp); |
157 | return count; |
158 | } |
159 | |
160 | static ssize_t store_amb_mid(struct device *dev, |
161 | struct device_attribute *devattr, |
162 | const char *buf, |
163 | size_t count) |
164 | { |
165 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
166 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
167 | unsigned long temp; |
168 | int ret = kstrtoul(s: buf, base: 10, res: &temp); |
169 | if (ret < 0) |
170 | return ret; |
171 | |
172 | temp = temp / 500; |
173 | if (temp > 255) |
174 | temp = 255; |
175 | |
176 | amb_write_byte(data, offset: amb_reg_temp_mid(amb: attr->index), val: temp); |
177 | return count; |
178 | } |
179 | |
180 | static ssize_t store_amb_max(struct device *dev, |
181 | struct device_attribute *devattr, |
182 | const char *buf, |
183 | size_t count) |
184 | { |
185 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
186 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
187 | unsigned long temp; |
188 | int ret = kstrtoul(s: buf, base: 10, res: &temp); |
189 | if (ret < 0) |
190 | return ret; |
191 | |
192 | temp = temp / 500; |
193 | if (temp > 255) |
194 | temp = 255; |
195 | |
196 | amb_write_byte(data, offset: amb_reg_temp_max(amb: attr->index), val: temp); |
197 | return count; |
198 | } |
199 | |
200 | static ssize_t show_amb_min(struct device *dev, |
201 | struct device_attribute *devattr, |
202 | char *buf) |
203 | { |
204 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
205 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
206 | return sprintf(buf, fmt: "%d\n" , |
207 | 500 * amb_read_byte(data, offset: amb_reg_temp_min(amb: attr->index))); |
208 | } |
209 | |
210 | static ssize_t show_amb_mid(struct device *dev, |
211 | struct device_attribute *devattr, |
212 | char *buf) |
213 | { |
214 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
215 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
216 | return sprintf(buf, fmt: "%d\n" , |
217 | 500 * amb_read_byte(data, offset: amb_reg_temp_mid(amb: attr->index))); |
218 | } |
219 | |
220 | static ssize_t show_amb_max(struct device *dev, |
221 | struct device_attribute *devattr, |
222 | char *buf) |
223 | { |
224 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
225 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
226 | return sprintf(buf, fmt: "%d\n" , |
227 | 500 * amb_read_byte(data, offset: amb_reg_temp_max(amb: attr->index))); |
228 | } |
229 | |
230 | static ssize_t show_amb_temp(struct device *dev, |
231 | struct device_attribute *devattr, |
232 | char *buf) |
233 | { |
234 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
235 | struct i5k_amb_data *data = dev_get_drvdata(dev); |
236 | return sprintf(buf, fmt: "%d\n" , |
237 | 500 * amb_read_byte(data, offset: amb_reg_temp(amb: attr->index))); |
238 | } |
239 | |
240 | static ssize_t show_label(struct device *dev, |
241 | struct device_attribute *devattr, |
242 | char *buf) |
243 | { |
244 | struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); |
245 | |
246 | return sprintf(buf, fmt: "Ch. %d DIMM %d\n" , attr->index >> CHANNEL_SHIFT, |
247 | attr->index & DIMM_MASK); |
248 | } |
249 | |
250 | static int i5k_amb_hwmon_init(struct platform_device *pdev) |
251 | { |
252 | int i, j, k, d = 0; |
253 | u16 c; |
254 | int res = 0; |
255 | int num_ambs = 0; |
256 | struct i5k_amb_data *data = platform_get_drvdata(pdev); |
257 | |
258 | /* Count the number of AMBs found */ |
259 | /* ignore the high-order bit, see "Ugly hack" comment above */ |
260 | for (i = 0; i < MAX_MEM_CHANNELS; i++) |
261 | num_ambs += hweight16(data->amb_present[i] & 0x7fff); |
262 | |
263 | /* Set up sysfs stuff */ |
264 | data->attrs = kzalloc(array3_size(num_ambs, KNOBS_PER_AMB, |
265 | sizeof(*data->attrs)), |
266 | GFP_KERNEL); |
267 | if (!data->attrs) |
268 | return -ENOMEM; |
269 | data->num_attrs = 0; |
270 | |
271 | for (i = 0; i < MAX_MEM_CHANNELS; i++) { |
272 | c = data->amb_present[i]; |
273 | for (j = 0; j < REAL_MAX_AMBS_PER_CHANNEL; j++, c >>= 1) { |
274 | struct i5k_device_attribute *iattr; |
275 | |
276 | k = amb_num_from_reg(byte_num: i, bit: j); |
277 | if (!(c & 0x1)) |
278 | continue; |
279 | d++; |
280 | |
281 | /* sysfs label */ |
282 | iattr = data->attrs + data->num_attrs; |
283 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
284 | fmt: "temp%d_label" , d); |
285 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
286 | iattr->s_attr.dev_attr.attr.mode = 0444; |
287 | iattr->s_attr.dev_attr.show = show_label; |
288 | iattr->s_attr.index = k; |
289 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
290 | res = device_create_file(device: &pdev->dev, |
291 | entry: &iattr->s_attr.dev_attr); |
292 | if (res) |
293 | goto exit_remove; |
294 | data->num_attrs++; |
295 | |
296 | /* Temperature sysfs knob */ |
297 | iattr = data->attrs + data->num_attrs; |
298 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
299 | fmt: "temp%d_input" , d); |
300 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
301 | iattr->s_attr.dev_attr.attr.mode = 0444; |
302 | iattr->s_attr.dev_attr.show = show_amb_temp; |
303 | iattr->s_attr.index = k; |
304 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
305 | res = device_create_file(device: &pdev->dev, |
306 | entry: &iattr->s_attr.dev_attr); |
307 | if (res) |
308 | goto exit_remove; |
309 | data->num_attrs++; |
310 | |
311 | /* Temperature min sysfs knob */ |
312 | iattr = data->attrs + data->num_attrs; |
313 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
314 | fmt: "temp%d_min" , d); |
315 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
316 | iattr->s_attr.dev_attr.attr.mode = 0644; |
317 | iattr->s_attr.dev_attr.show = show_amb_min; |
318 | iattr->s_attr.dev_attr.store = store_amb_min; |
319 | iattr->s_attr.index = k; |
320 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
321 | res = device_create_file(device: &pdev->dev, |
322 | entry: &iattr->s_attr.dev_attr); |
323 | if (res) |
324 | goto exit_remove; |
325 | data->num_attrs++; |
326 | |
327 | /* Temperature mid sysfs knob */ |
328 | iattr = data->attrs + data->num_attrs; |
329 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
330 | fmt: "temp%d_mid" , d); |
331 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
332 | iattr->s_attr.dev_attr.attr.mode = 0644; |
333 | iattr->s_attr.dev_attr.show = show_amb_mid; |
334 | iattr->s_attr.dev_attr.store = store_amb_mid; |
335 | iattr->s_attr.index = k; |
336 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
337 | res = device_create_file(device: &pdev->dev, |
338 | entry: &iattr->s_attr.dev_attr); |
339 | if (res) |
340 | goto exit_remove; |
341 | data->num_attrs++; |
342 | |
343 | /* Temperature max sysfs knob */ |
344 | iattr = data->attrs + data->num_attrs; |
345 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
346 | fmt: "temp%d_max" , d); |
347 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
348 | iattr->s_attr.dev_attr.attr.mode = 0644; |
349 | iattr->s_attr.dev_attr.show = show_amb_max; |
350 | iattr->s_attr.dev_attr.store = store_amb_max; |
351 | iattr->s_attr.index = k; |
352 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
353 | res = device_create_file(device: &pdev->dev, |
354 | entry: &iattr->s_attr.dev_attr); |
355 | if (res) |
356 | goto exit_remove; |
357 | data->num_attrs++; |
358 | |
359 | /* Temperature alarm sysfs knob */ |
360 | iattr = data->attrs + data->num_attrs; |
361 | snprintf(buf: iattr->name, AMB_SYSFS_NAME_LEN, |
362 | fmt: "temp%d_alarm" , d); |
363 | iattr->s_attr.dev_attr.attr.name = iattr->name; |
364 | iattr->s_attr.dev_attr.attr.mode = 0444; |
365 | iattr->s_attr.dev_attr.show = show_amb_alarm; |
366 | iattr->s_attr.index = k; |
367 | sysfs_attr_init(&iattr->s_attr.dev_attr.attr); |
368 | res = device_create_file(device: &pdev->dev, |
369 | entry: &iattr->s_attr.dev_attr); |
370 | if (res) |
371 | goto exit_remove; |
372 | data->num_attrs++; |
373 | } |
374 | } |
375 | |
376 | res = device_create_file(device: &pdev->dev, entry: &dev_attr_name); |
377 | if (res) |
378 | goto exit_remove; |
379 | |
380 | data->hwmon_dev = hwmon_device_register(dev: &pdev->dev); |
381 | if (IS_ERR(ptr: data->hwmon_dev)) { |
382 | res = PTR_ERR(ptr: data->hwmon_dev); |
383 | goto exit_remove; |
384 | } |
385 | |
386 | return res; |
387 | |
388 | exit_remove: |
389 | device_remove_file(dev: &pdev->dev, attr: &dev_attr_name); |
390 | for (i = 0; i < data->num_attrs; i++) |
391 | device_remove_file(dev: &pdev->dev, attr: &data->attrs[i].s_attr.dev_attr); |
392 | kfree(objp: data->attrs); |
393 | |
394 | return res; |
395 | } |
396 | |
397 | static int i5k_amb_add(void) |
398 | { |
399 | int res; |
400 | |
401 | /* only ever going to be one of these */ |
402 | amb_pdev = platform_device_alloc(DRVNAME, id: 0); |
403 | if (!amb_pdev) |
404 | return -ENOMEM; |
405 | |
406 | res = platform_device_add(pdev: amb_pdev); |
407 | if (res) |
408 | goto err; |
409 | return 0; |
410 | |
411 | err: |
412 | platform_device_put(pdev: amb_pdev); |
413 | return res; |
414 | } |
415 | |
416 | static int i5k_find_amb_registers(struct i5k_amb_data *data, |
417 | unsigned long devid) |
418 | { |
419 | struct pci_dev *pcidev; |
420 | u32 val32; |
421 | int res = -ENODEV; |
422 | |
423 | /* Find AMB register memory space */ |
424 | pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, |
425 | device: devid, |
426 | NULL); |
427 | if (!pcidev) |
428 | return -ENODEV; |
429 | |
430 | pci_read_config_dword(dev: pcidev, I5K_REG_AMB_BASE_ADDR, val: &val32); |
431 | if (val32 == (u32)~0) |
432 | goto out; |
433 | data->amb_base = val32; |
434 | |
435 | pci_read_config_dword(dev: pcidev, I5K_REG_AMB_LEN_ADDR, val: &val32); |
436 | if (val32 == (u32)~0) |
437 | goto out; |
438 | data->amb_len = val32; |
439 | |
440 | /* Is it big enough? */ |
441 | if (data->amb_len < AMB_CONFIG_SIZE * MAX_AMBS) { |
442 | dev_err(&pcidev->dev, "AMB region too small!\n" ); |
443 | goto out; |
444 | } |
445 | |
446 | res = 0; |
447 | out: |
448 | pci_dev_put(dev: pcidev); |
449 | return res; |
450 | } |
451 | |
452 | static int i5k_channel_probe(u16 *amb_present, unsigned long dev_id) |
453 | { |
454 | struct pci_dev *pcidev; |
455 | u16 val16; |
456 | int res = -ENODEV; |
457 | |
458 | /* Copy the DIMM presence map for these two channels */ |
459 | pcidev = pci_get_device(PCI_VENDOR_ID_INTEL, device: dev_id, NULL); |
460 | if (!pcidev) |
461 | return -ENODEV; |
462 | |
463 | pci_read_config_word(dev: pcidev, I5K_REG_CHAN0_PRESENCE_ADDR, val: &val16); |
464 | if (val16 == (u16)~0) |
465 | goto out; |
466 | amb_present[0] = val16; |
467 | |
468 | pci_read_config_word(dev: pcidev, I5K_REG_CHAN1_PRESENCE_ADDR, val: &val16); |
469 | if (val16 == (u16)~0) |
470 | goto out; |
471 | amb_present[1] = val16; |
472 | |
473 | res = 0; |
474 | |
475 | out: |
476 | pci_dev_put(dev: pcidev); |
477 | return res; |
478 | } |
479 | |
480 | static struct { |
481 | unsigned long err; |
482 | unsigned long fbd0; |
483 | } chipset_ids[] = { |
484 | { PCI_DEVICE_ID_INTEL_5000_ERR, PCI_DEVICE_ID_INTEL_5000_FBD0 }, |
485 | { PCI_DEVICE_ID_INTEL_5400_ERR, PCI_DEVICE_ID_INTEL_5400_FBD0 }, |
486 | { 0, 0 } |
487 | }; |
488 | |
489 | #ifdef MODULE |
490 | static const struct pci_device_id i5k_amb_ids[] = { |
491 | { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_5000_ERR) }, |
492 | { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_5400_ERR) }, |
493 | { 0, } |
494 | }; |
495 | MODULE_DEVICE_TABLE(pci, i5k_amb_ids); |
496 | #endif |
497 | |
498 | static int i5k_amb_probe(struct platform_device *pdev) |
499 | { |
500 | struct i5k_amb_data *data; |
501 | struct resource *reso; |
502 | int i, res; |
503 | |
504 | data = kzalloc(size: sizeof(*data), GFP_KERNEL); |
505 | if (!data) |
506 | return -ENOMEM; |
507 | |
508 | /* Figure out where the AMB registers live */ |
509 | i = 0; |
510 | do { |
511 | res = i5k_find_amb_registers(data, devid: chipset_ids[i].err); |
512 | if (res == 0) |
513 | break; |
514 | i++; |
515 | } while (chipset_ids[i].err); |
516 | |
517 | if (res) |
518 | goto err; |
519 | |
520 | /* Copy the DIMM presence map for the first two channels */ |
521 | res = i5k_channel_probe(amb_present: &data->amb_present[0], dev_id: chipset_ids[i].fbd0); |
522 | if (res) |
523 | goto err; |
524 | |
525 | /* Copy the DIMM presence map for the optional second two channels */ |
526 | i5k_channel_probe(amb_present: &data->amb_present[2], dev_id: chipset_ids[i].fbd0 + 1); |
527 | |
528 | /* Set up resource regions */ |
529 | reso = request_mem_region(data->amb_base, data->amb_len, DRVNAME); |
530 | if (!reso) { |
531 | res = -EBUSY; |
532 | goto err; |
533 | } |
534 | |
535 | data->amb_mmio = ioremap(offset: data->amb_base, size: data->amb_len); |
536 | if (!data->amb_mmio) { |
537 | res = -EBUSY; |
538 | goto err_map_failed; |
539 | } |
540 | |
541 | platform_set_drvdata(pdev, data); |
542 | |
543 | res = i5k_amb_hwmon_init(pdev); |
544 | if (res) |
545 | goto err_init_failed; |
546 | |
547 | return res; |
548 | |
549 | err_init_failed: |
550 | iounmap(addr: data->amb_mmio); |
551 | err_map_failed: |
552 | release_mem_region(data->amb_base, data->amb_len); |
553 | err: |
554 | kfree(objp: data); |
555 | return res; |
556 | } |
557 | |
558 | static void i5k_amb_remove(struct platform_device *pdev) |
559 | { |
560 | int i; |
561 | struct i5k_amb_data *data = platform_get_drvdata(pdev); |
562 | |
563 | hwmon_device_unregister(dev: data->hwmon_dev); |
564 | device_remove_file(dev: &pdev->dev, attr: &dev_attr_name); |
565 | for (i = 0; i < data->num_attrs; i++) |
566 | device_remove_file(dev: &pdev->dev, attr: &data->attrs[i].s_attr.dev_attr); |
567 | kfree(objp: data->attrs); |
568 | iounmap(addr: data->amb_mmio); |
569 | release_mem_region(data->amb_base, data->amb_len); |
570 | kfree(objp: data); |
571 | } |
572 | |
573 | static struct platform_driver i5k_amb_driver = { |
574 | .driver = { |
575 | .name = DRVNAME, |
576 | }, |
577 | .probe = i5k_amb_probe, |
578 | .remove_new = i5k_amb_remove, |
579 | }; |
580 | |
581 | static int __init i5k_amb_init(void) |
582 | { |
583 | int res; |
584 | |
585 | res = platform_driver_register(&i5k_amb_driver); |
586 | if (res) |
587 | return res; |
588 | |
589 | res = i5k_amb_add(); |
590 | if (res) |
591 | platform_driver_unregister(&i5k_amb_driver); |
592 | |
593 | return res; |
594 | } |
595 | |
596 | static void __exit i5k_amb_exit(void) |
597 | { |
598 | platform_device_unregister(amb_pdev); |
599 | platform_driver_unregister(&i5k_amb_driver); |
600 | } |
601 | |
602 | MODULE_AUTHOR("Darrick J. Wong <darrick.wong@oracle.com>" ); |
603 | MODULE_DESCRIPTION("Intel 5000 chipset FB-DIMM AMB temperature sensor" ); |
604 | MODULE_LICENSE("GPL" ); |
605 | |
606 | module_init(i5k_amb_init); |
607 | module_exit(i5k_amb_exit); |
608 | |