1 |
62 |
marcus.erl |
/* fschmd.c
|
2 |
|
|
*
|
3 |
|
|
* Copyright (C) 2007 Hans de Goede <j.w.r.degoede@hhs.nl>
|
4 |
|
|
*
|
5 |
|
|
* This program is free software; you can redistribute it and/or modify
|
6 |
|
|
* it under the terms of the GNU General Public License as published by
|
7 |
|
|
* the Free Software Foundation; either version 2 of the License, or
|
8 |
|
|
* (at your option) any later version.
|
9 |
|
|
*
|
10 |
|
|
* This program is distributed in the hope that it will be useful,
|
11 |
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12 |
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13 |
|
|
* GNU General Public License for more details.
|
14 |
|
|
*
|
15 |
|
|
* You should have received a copy of the GNU General Public License
|
16 |
|
|
* along with this program; if not, write to the Free Software
|
17 |
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
18 |
|
|
*/
|
19 |
|
|
|
20 |
|
|
/*
|
21 |
|
|
* Merged Fujitsu Siemens hwmon driver, supporting the Poseidon, Hermes,
|
22 |
|
|
* Scylla, Heracles and Heimdall chips
|
23 |
|
|
*
|
24 |
|
|
* Based on the original 2.4 fscscy, 2.6 fscpos, 2.6 fscher and 2.6
|
25 |
|
|
* (candidate) fschmd drivers:
|
26 |
|
|
* Copyright (C) 2006 Thilo Cestonaro
|
27 |
|
|
* <thilo.cestonaro.external@fujitsu-siemens.com>
|
28 |
|
|
* Copyright (C) 2004, 2005 Stefan Ott <stefan@desire.ch>
|
29 |
|
|
* Copyright (C) 2003, 2004 Reinhard Nissl <rnissl@gmx.de>
|
30 |
|
|
* Copyright (c) 2001 Martin Knoblauch <mkn@teraport.de, knobi@knobisoft.de>
|
31 |
|
|
* Copyright (C) 2000 Hermann Jung <hej@odn.de>
|
32 |
|
|
*/
|
33 |
|
|
|
34 |
|
|
#include <linux/module.h>
|
35 |
|
|
#include <linux/init.h>
|
36 |
|
|
#include <linux/slab.h>
|
37 |
|
|
#include <linux/jiffies.h>
|
38 |
|
|
#include <linux/i2c.h>
|
39 |
|
|
#include <linux/hwmon.h>
|
40 |
|
|
#include <linux/hwmon-sysfs.h>
|
41 |
|
|
#include <linux/err.h>
|
42 |
|
|
#include <linux/mutex.h>
|
43 |
|
|
#include <linux/sysfs.h>
|
44 |
|
|
|
45 |
|
|
/* Addresses to scan */
|
46 |
|
|
static unsigned short normal_i2c[] = { 0x73, I2C_CLIENT_END };
|
47 |
|
|
|
48 |
|
|
/* Insmod parameters */
|
49 |
|
|
I2C_CLIENT_INSMOD_5(fscpos, fscher, fscscy, fschrc, fschmd);
|
50 |
|
|
|
51 |
|
|
/*
|
52 |
|
|
* The FSCHMD registers and other defines
|
53 |
|
|
*/
|
54 |
|
|
|
55 |
|
|
/* chip identification */
|
56 |
|
|
#define FSCHMD_REG_IDENT_0 0x00
|
57 |
|
|
#define FSCHMD_REG_IDENT_1 0x01
|
58 |
|
|
#define FSCHMD_REG_IDENT_2 0x02
|
59 |
|
|
#define FSCHMD_REG_REVISION 0x03
|
60 |
|
|
|
61 |
|
|
/* global control and status */
|
62 |
|
|
#define FSCHMD_REG_EVENT_STATE 0x04
|
63 |
|
|
#define FSCHMD_REG_CONTROL 0x05
|
64 |
|
|
|
65 |
|
|
#define FSCHMD_CONTROL_ALERT_LED_MASK 0x01
|
66 |
|
|
|
67 |
|
|
/* watchdog (support to be implemented) */
|
68 |
|
|
#define FSCHMD_REG_WDOG_PRESET 0x28
|
69 |
|
|
#define FSCHMD_REG_WDOG_STATE 0x23
|
70 |
|
|
#define FSCHMD_REG_WDOG_CONTROL 0x21
|
71 |
|
|
|
72 |
|
|
/* voltages, weird order is to keep the same order as the old drivers */
|
73 |
|
|
static const u8 FSCHMD_REG_VOLT[3] = { 0x45, 0x42, 0x48 };
|
74 |
|
|
|
75 |
|
|
/* minimum pwm at which the fan is driven (pwm can by increased depending on
|
76 |
|
|
the temp. Notice that for the scy some fans share there minimum speed.
|
77 |
|
|
Also notice that with the scy the sensor order is different then with the
|
78 |
|
|
other chips, this order was in the 2.4 driver and kept for consistency. */
|
79 |
|
|
static const u8 FSCHMD_REG_FAN_MIN[5][6] = {
|
80 |
|
|
{ 0x55, 0x65 }, /* pos */
|
81 |
|
|
{ 0x55, 0x65, 0xb5 }, /* her */
|
82 |
|
|
{ 0x65, 0x65, 0x55, 0xa5, 0x55, 0xa5 }, /* scy */
|
83 |
|
|
{ 0x55, 0x65, 0xa5, 0xb5 }, /* hrc */
|
84 |
|
|
{ 0x55, 0x65, 0xa5, 0xb5, 0xc5 }, /* hmd */
|
85 |
|
|
};
|
86 |
|
|
|
87 |
|
|
/* actual fan speed */
|
88 |
|
|
static const u8 FSCHMD_REG_FAN_ACT[5][6] = {
|
89 |
|
|
{ 0x0e, 0x6b, 0xab }, /* pos */
|
90 |
|
|
{ 0x0e, 0x6b, 0xbb }, /* her */
|
91 |
|
|
{ 0x6b, 0x6c, 0x0e, 0xab, 0x5c, 0xbb }, /* scy */
|
92 |
|
|
{ 0x0e, 0x6b, 0xab, 0xbb }, /* hrc */
|
93 |
|
|
{ 0x5b, 0x6b, 0xab, 0xbb, 0xcb }, /* hmd */
|
94 |
|
|
};
|
95 |
|
|
|
96 |
|
|
/* fan status registers */
|
97 |
|
|
static const u8 FSCHMD_REG_FAN_STATE[5][6] = {
|
98 |
|
|
{ 0x0d, 0x62, 0xa2 }, /* pos */
|
99 |
|
|
{ 0x0d, 0x62, 0xb2 }, /* her */
|
100 |
|
|
{ 0x62, 0x61, 0x0d, 0xa2, 0x52, 0xb2 }, /* scy */
|
101 |
|
|
{ 0x0d, 0x62, 0xa2, 0xb2 }, /* hrc */
|
102 |
|
|
{ 0x52, 0x62, 0xa2, 0xb2, 0xc2 }, /* hmd */
|
103 |
|
|
};
|
104 |
|
|
|
105 |
|
|
/* fan ripple / divider registers */
|
106 |
|
|
static const u8 FSCHMD_REG_FAN_RIPPLE[5][6] = {
|
107 |
|
|
{ 0x0f, 0x6f, 0xaf }, /* pos */
|
108 |
|
|
{ 0x0f, 0x6f, 0xbf }, /* her */
|
109 |
|
|
{ 0x6f, 0x6f, 0x0f, 0xaf, 0x0f, 0xbf }, /* scy */
|
110 |
|
|
{ 0x0f, 0x6f, 0xaf, 0xbf }, /* hrc */
|
111 |
|
|
{ 0x5f, 0x6f, 0xaf, 0xbf, 0xcf }, /* hmd */
|
112 |
|
|
};
|
113 |
|
|
|
114 |
|
|
static const int FSCHMD_NO_FAN_SENSORS[5] = { 3, 3, 6, 4, 5 };
|
115 |
|
|
|
116 |
|
|
/* Fan status register bitmasks */
|
117 |
|
|
#define FSCHMD_FAN_ALARM_MASK 0x04 /* called fault by FSC! */
|
118 |
|
|
#define FSCHMD_FAN_NOT_PRESENT_MASK 0x08 /* not documented */
|
119 |
|
|
|
120 |
|
|
|
121 |
|
|
/* actual temperature registers */
|
122 |
|
|
static const u8 FSCHMD_REG_TEMP_ACT[5][5] = {
|
123 |
|
|
{ 0x64, 0x32, 0x35 }, /* pos */
|
124 |
|
|
{ 0x64, 0x32, 0x35 }, /* her */
|
125 |
|
|
{ 0x64, 0xD0, 0x32, 0x35 }, /* scy */
|
126 |
|
|
{ 0x64, 0x32, 0x35 }, /* hrc */
|
127 |
|
|
{ 0x70, 0x80, 0x90, 0xd0, 0xe0 }, /* hmd */
|
128 |
|
|
};
|
129 |
|
|
|
130 |
|
|
/* temperature state registers */
|
131 |
|
|
static const u8 FSCHMD_REG_TEMP_STATE[5][5] = {
|
132 |
|
|
{ 0x71, 0x81, 0x91 }, /* pos */
|
133 |
|
|
{ 0x71, 0x81, 0x91 }, /* her */
|
134 |
|
|
{ 0x71, 0xd1, 0x81, 0x91 }, /* scy */
|
135 |
|
|
{ 0x71, 0x81, 0x91 }, /* hrc */
|
136 |
|
|
{ 0x71, 0x81, 0x91, 0xd1, 0xe1 }, /*Â hmd */
|
137 |
|
|
};
|
138 |
|
|
|
139 |
|
|
/* temperature high limit registers, FSC does not document these. Proven to be
|
140 |
|
|
there with field testing on the fscher and fschrc, already supported / used
|
141 |
|
|
in the fscscy 2.4 driver. FSC has confirmed that the fschmd has registers
|
142 |
|
|
at these addresses, but doesn't want to confirm they are the same as with
|
143 |
|
|
the fscher?? */
|
144 |
|
|
static const u8 FSCHMD_REG_TEMP_LIMIT[5][5] = {
|
145 |
|
|
{ 0, 0, 0 }, /* pos */
|
146 |
|
|
{ 0x76, 0x86, 0x96 }, /* her */
|
147 |
|
|
{ 0x76, 0xd6, 0x86, 0x96 }, /* scy */
|
148 |
|
|
{ 0x76, 0x86, 0x96 }, /* hrc */
|
149 |
|
|
{ 0x76, 0x86, 0x96, 0xd6, 0xe6 }, /*Â hmd */
|
150 |
|
|
};
|
151 |
|
|
|
152 |
|
|
/* These were found through experimenting with an fscher, currently they are
|
153 |
|
|
not used, but we keep them around for future reference.
|
154 |
|
|
static const u8 FSCHER_REG_TEMP_AUTOP1[] = { 0x73, 0x83, 0x93 };
|
155 |
|
|
static const u8 FSCHER_REG_TEMP_AUTOP2[] = { 0x75, 0x85, 0x95 }; */
|
156 |
|
|
|
157 |
|
|
static const int FSCHMD_NO_TEMP_SENSORS[5] = { 3, 3, 4, 3, 5 };
|
158 |
|
|
|
159 |
|
|
/* temp status register bitmasks */
|
160 |
|
|
#define FSCHMD_TEMP_WORKING_MASK 0x01
|
161 |
|
|
#define FSCHMD_TEMP_ALERT_MASK 0x02
|
162 |
|
|
/* there only really is an alarm if the sensor is working and alert == 1 */
|
163 |
|
|
#define FSCHMD_TEMP_ALARM_MASK \
|
164 |
|
|
(FSCHMD_TEMP_WORKING_MASK | FSCHMD_TEMP_ALERT_MASK)
|
165 |
|
|
|
166 |
|
|
/* our driver name */
|
167 |
|
|
#define FSCHMD_NAME "fschmd"
|
168 |
|
|
|
169 |
|
|
/*
|
170 |
|
|
* Functions declarations
|
171 |
|
|
*/
|
172 |
|
|
|
173 |
|
|
static int fschmd_attach_adapter(struct i2c_adapter *adapter);
|
174 |
|
|
static int fschmd_detach_client(struct i2c_client *client);
|
175 |
|
|
static struct fschmd_data *fschmd_update_device(struct device *dev);
|
176 |
|
|
|
177 |
|
|
/*
|
178 |
|
|
* Driver data (common to all clients)
|
179 |
|
|
*/
|
180 |
|
|
|
181 |
|
|
static struct i2c_driver fschmd_driver = {
|
182 |
|
|
.driver = {
|
183 |
|
|
.name = FSCHMD_NAME,
|
184 |
|
|
},
|
185 |
|
|
.attach_adapter = fschmd_attach_adapter,
|
186 |
|
|
.detach_client = fschmd_detach_client,
|
187 |
|
|
};
|
188 |
|
|
|
189 |
|
|
/*
|
190 |
|
|
* Client data (each client gets its own)
|
191 |
|
|
*/
|
192 |
|
|
|
193 |
|
|
struct fschmd_data {
|
194 |
|
|
struct i2c_client client;
|
195 |
|
|
struct device *hwmon_dev;
|
196 |
|
|
struct mutex update_lock;
|
197 |
|
|
int kind;
|
198 |
|
|
char valid; /* zero until following fields are valid */
|
199 |
|
|
unsigned long last_updated; /* in jiffies */
|
200 |
|
|
|
201 |
|
|
/* register values */
|
202 |
|
|
u8 global_control; /* global control register */
|
203 |
|
|
u8 volt[3]; /* 12, 5, battery voltage */
|
204 |
|
|
u8 temp_act[5]; /* temperature */
|
205 |
|
|
u8 temp_status[5]; /* status of sensor */
|
206 |
|
|
u8 temp_max[5]; /* high temp limit, notice: undocumented! */
|
207 |
|
|
u8 fan_act[6]; /* fans revolutions per second */
|
208 |
|
|
u8 fan_status[6]; /* fan status */
|
209 |
|
|
u8 fan_min[6]; /* fan min value for rps */
|
210 |
|
|
u8 fan_ripple[6]; /* divider for rps */
|
211 |
|
|
};
|
212 |
|
|
|
213 |
|
|
/*
|
214 |
|
|
* Sysfs attr show / store functions
|
215 |
|
|
*/
|
216 |
|
|
|
217 |
|
|
static ssize_t show_in_value(struct device *dev,
|
218 |
|
|
struct device_attribute *devattr, char *buf)
|
219 |
|
|
{
|
220 |
|
|
const int max_reading[3] = { 14200, 6600, 3300 };
|
221 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
222 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
223 |
|
|
|
224 |
|
|
return sprintf(buf, "%d\n", (data->volt[index] *
|
225 |
|
|
max_reading[index] + 128) / 255);
|
226 |
|
|
}
|
227 |
|
|
|
228 |
|
|
|
229 |
|
|
#define TEMP_FROM_REG(val) (((val) - 128) * 1000)
|
230 |
|
|
|
231 |
|
|
static ssize_t show_temp_value(struct device *dev,
|
232 |
|
|
struct device_attribute *devattr, char *buf)
|
233 |
|
|
{
|
234 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
235 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
236 |
|
|
|
237 |
|
|
return sprintf(buf, "%d\n", TEMP_FROM_REG(data->temp_act[index]));
|
238 |
|
|
}
|
239 |
|
|
|
240 |
|
|
static ssize_t show_temp_max(struct device *dev,
|
241 |
|
|
struct device_attribute *devattr, char *buf)
|
242 |
|
|
{
|
243 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
244 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
245 |
|
|
|
246 |
|
|
return sprintf(buf, "%d\n", TEMP_FROM_REG(data->temp_max[index]));
|
247 |
|
|
}
|
248 |
|
|
|
249 |
|
|
static ssize_t store_temp_max(struct device *dev, struct device_attribute
|
250 |
|
|
*devattr, const char *buf, size_t count)
|
251 |
|
|
{
|
252 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
253 |
|
|
struct fschmd_data *data = dev_get_drvdata(dev);
|
254 |
|
|
long v = simple_strtol(buf, NULL, 10) / 1000;
|
255 |
|
|
|
256 |
|
|
v = SENSORS_LIMIT(v, -128, 127) + 128;
|
257 |
|
|
|
258 |
|
|
mutex_lock(&data->update_lock);
|
259 |
|
|
i2c_smbus_write_byte_data(&data->client,
|
260 |
|
|
FSCHMD_REG_TEMP_LIMIT[data->kind][index], v);
|
261 |
|
|
data->temp_max[index] = v;
|
262 |
|
|
mutex_unlock(&data->update_lock);
|
263 |
|
|
|
264 |
|
|
return count;
|
265 |
|
|
}
|
266 |
|
|
|
267 |
|
|
static ssize_t show_temp_fault(struct device *dev,
|
268 |
|
|
struct device_attribute *devattr, char *buf)
|
269 |
|
|
{
|
270 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
271 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
272 |
|
|
|
273 |
|
|
/* bit 0 set means sensor working ok, so no fault! */
|
274 |
|
|
if (data->temp_status[index] & FSCHMD_TEMP_WORKING_MASK)
|
275 |
|
|
return sprintf(buf, "0\n");
|
276 |
|
|
else
|
277 |
|
|
return sprintf(buf, "1\n");
|
278 |
|
|
}
|
279 |
|
|
|
280 |
|
|
static ssize_t show_temp_alarm(struct device *dev,
|
281 |
|
|
struct device_attribute *devattr, char *buf)
|
282 |
|
|
{
|
283 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
284 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
285 |
|
|
|
286 |
|
|
if ((data->temp_status[index] & FSCHMD_TEMP_ALARM_MASK) ==
|
287 |
|
|
FSCHMD_TEMP_ALARM_MASK)
|
288 |
|
|
return sprintf(buf, "1\n");
|
289 |
|
|
else
|
290 |
|
|
return sprintf(buf, "0\n");
|
291 |
|
|
}
|
292 |
|
|
|
293 |
|
|
|
294 |
|
|
#define RPM_FROM_REG(val) ((val) * 60)
|
295 |
|
|
|
296 |
|
|
static ssize_t show_fan_value(struct device *dev,
|
297 |
|
|
struct device_attribute *devattr, char *buf)
|
298 |
|
|
{
|
299 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
300 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
301 |
|
|
|
302 |
|
|
return sprintf(buf, "%u\n", RPM_FROM_REG(data->fan_act[index]));
|
303 |
|
|
}
|
304 |
|
|
|
305 |
|
|
static ssize_t show_fan_div(struct device *dev,
|
306 |
|
|
struct device_attribute *devattr, char *buf)
|
307 |
|
|
{
|
308 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
309 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
310 |
|
|
|
311 |
|
|
/* bits 2..7 reserved => mask with 3 */
|
312 |
|
|
return sprintf(buf, "%d\n", 1 << (data->fan_ripple[index] & 3));
|
313 |
|
|
}
|
314 |
|
|
|
315 |
|
|
static ssize_t store_fan_div(struct device *dev, struct device_attribute
|
316 |
|
|
*devattr, const char *buf, size_t count)
|
317 |
|
|
{
|
318 |
|
|
u8 reg;
|
319 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
320 |
|
|
struct fschmd_data *data = dev_get_drvdata(dev);
|
321 |
|
|
/* supported values: 2, 4, 8 */
|
322 |
|
|
unsigned long v = simple_strtoul(buf, NULL, 10);
|
323 |
|
|
|
324 |
|
|
switch (v) {
|
325 |
|
|
case 2: v = 1; break;
|
326 |
|
|
case 4: v = 2; break;
|
327 |
|
|
case 8: v = 3; break;
|
328 |
|
|
default:
|
329 |
|
|
dev_err(dev, "fan_div value %lu not supported. "
|
330 |
|
|
"Choose one of 2, 4 or 8!\n", v);
|
331 |
|
|
return -EINVAL;
|
332 |
|
|
}
|
333 |
|
|
|
334 |
|
|
mutex_lock(&data->update_lock);
|
335 |
|
|
|
336 |
|
|
reg = i2c_smbus_read_byte_data(&data->client,
|
337 |
|
|
FSCHMD_REG_FAN_RIPPLE[data->kind][index]);
|
338 |
|
|
|
339 |
|
|
/* bits 2..7 reserved => mask with 0x03 */
|
340 |
|
|
reg &= ~0x03;
|
341 |
|
|
reg |= v;
|
342 |
|
|
|
343 |
|
|
i2c_smbus_write_byte_data(&data->client,
|
344 |
|
|
FSCHMD_REG_FAN_RIPPLE[data->kind][index], reg);
|
345 |
|
|
|
346 |
|
|
data->fan_ripple[index] = reg;
|
347 |
|
|
|
348 |
|
|
mutex_unlock(&data->update_lock);
|
349 |
|
|
|
350 |
|
|
return count;
|
351 |
|
|
}
|
352 |
|
|
|
353 |
|
|
static ssize_t show_fan_alarm(struct device *dev,
|
354 |
|
|
struct device_attribute *devattr, char *buf)
|
355 |
|
|
{
|
356 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
357 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
358 |
|
|
|
359 |
|
|
if (data->fan_status[index] & FSCHMD_FAN_ALARM_MASK)
|
360 |
|
|
return sprintf(buf, "1\n");
|
361 |
|
|
else
|
362 |
|
|
return sprintf(buf, "0\n");
|
363 |
|
|
}
|
364 |
|
|
|
365 |
|
|
static ssize_t show_fan_fault(struct device *dev,
|
366 |
|
|
struct device_attribute *devattr, char *buf)
|
367 |
|
|
{
|
368 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
369 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
370 |
|
|
|
371 |
|
|
if (data->fan_status[index] & FSCHMD_FAN_NOT_PRESENT_MASK)
|
372 |
|
|
return sprintf(buf, "1\n");
|
373 |
|
|
else
|
374 |
|
|
return sprintf(buf, "0\n");
|
375 |
|
|
}
|
376 |
|
|
|
377 |
|
|
|
378 |
|
|
static ssize_t show_pwm_auto_point1_pwm(struct device *dev,
|
379 |
|
|
struct device_attribute *devattr, char *buf)
|
380 |
|
|
{
|
381 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
382 |
|
|
int val = fschmd_update_device(dev)->fan_min[index];
|
383 |
|
|
|
384 |
|
|
/* 0 = allow turning off, 1-255 = 50-100% */
|
385 |
|
|
if (val)
|
386 |
|
|
val = val / 2 + 128;
|
387 |
|
|
|
388 |
|
|
return sprintf(buf, "%d\n", val);
|
389 |
|
|
}
|
390 |
|
|
|
391 |
|
|
static ssize_t store_pwm_auto_point1_pwm(struct device *dev,
|
392 |
|
|
struct device_attribute *devattr, const char *buf, size_t count)
|
393 |
|
|
{
|
394 |
|
|
int index = to_sensor_dev_attr(devattr)->index;
|
395 |
|
|
struct fschmd_data *data = dev_get_drvdata(dev);
|
396 |
|
|
unsigned long v = simple_strtoul(buf, NULL, 10);
|
397 |
|
|
|
398 |
|
|
/* register: 0 = allow turning off, 1-255 = 50-100% */
|
399 |
|
|
if (v) {
|
400 |
|
|
v = SENSORS_LIMIT(v, 128, 255);
|
401 |
|
|
v = (v - 128) * 2 + 1;
|
402 |
|
|
}
|
403 |
|
|
|
404 |
|
|
mutex_lock(&data->update_lock);
|
405 |
|
|
|
406 |
|
|
i2c_smbus_write_byte_data(&data->client,
|
407 |
|
|
FSCHMD_REG_FAN_MIN[data->kind][index], v);
|
408 |
|
|
data->fan_min[index] = v;
|
409 |
|
|
|
410 |
|
|
mutex_unlock(&data->update_lock);
|
411 |
|
|
|
412 |
|
|
return count;
|
413 |
|
|
}
|
414 |
|
|
|
415 |
|
|
|
416 |
|
|
/* The FSC hwmon family has the ability to force an attached alert led to flash
|
417 |
|
|
from software, we export this as an alert_led sysfs attr */
|
418 |
|
|
static ssize_t show_alert_led(struct device *dev,
|
419 |
|
|
struct device_attribute *devattr, char *buf)
|
420 |
|
|
{
|
421 |
|
|
struct fschmd_data *data = fschmd_update_device(dev);
|
422 |
|
|
|
423 |
|
|
if (data->global_control & FSCHMD_CONTROL_ALERT_LED_MASK)
|
424 |
|
|
return sprintf(buf, "1\n");
|
425 |
|
|
else
|
426 |
|
|
return sprintf(buf, "0\n");
|
427 |
|
|
}
|
428 |
|
|
|
429 |
|
|
static ssize_t store_alert_led(struct device *dev,
|
430 |
|
|
struct device_attribute *devattr, const char *buf, size_t count)
|
431 |
|
|
{
|
432 |
|
|
u8 reg;
|
433 |
|
|
struct fschmd_data *data = dev_get_drvdata(dev);
|
434 |
|
|
unsigned long v = simple_strtoul(buf, NULL, 10);
|
435 |
|
|
|
436 |
|
|
mutex_lock(&data->update_lock);
|
437 |
|
|
|
438 |
|
|
reg = i2c_smbus_read_byte_data(&data->client, FSCHMD_REG_CONTROL);
|
439 |
|
|
|
440 |
|
|
if (v)
|
441 |
|
|
reg |= FSCHMD_CONTROL_ALERT_LED_MASK;
|
442 |
|
|
else
|
443 |
|
|
reg &= ~FSCHMD_CONTROL_ALERT_LED_MASK;
|
444 |
|
|
|
445 |
|
|
i2c_smbus_write_byte_data(&data->client, FSCHMD_REG_CONTROL, reg);
|
446 |
|
|
|
447 |
|
|
data->global_control = reg;
|
448 |
|
|
|
449 |
|
|
mutex_unlock(&data->update_lock);
|
450 |
|
|
|
451 |
|
|
return count;
|
452 |
|
|
}
|
453 |
|
|
|
454 |
|
|
static struct sensor_device_attribute fschmd_attr[] = {
|
455 |
|
|
SENSOR_ATTR(in0_input, 0444, show_in_value, NULL, 0),
|
456 |
|
|
SENSOR_ATTR(in1_input, 0444, show_in_value, NULL, 1),
|
457 |
|
|
SENSOR_ATTR(in2_input, 0444, show_in_value, NULL, 2),
|
458 |
|
|
SENSOR_ATTR(alert_led, 0644, show_alert_led, store_alert_led, 0),
|
459 |
|
|
};
|
460 |
|
|
|
461 |
|
|
static struct sensor_device_attribute fschmd_temp_attr[] = {
|
462 |
|
|
SENSOR_ATTR(temp1_input, 0444, show_temp_value, NULL, 0),
|
463 |
|
|
SENSOR_ATTR(temp1_max, 0644, show_temp_max, store_temp_max, 0),
|
464 |
|
|
SENSOR_ATTR(temp1_fault, 0444, show_temp_fault, NULL, 0),
|
465 |
|
|
SENSOR_ATTR(temp1_alarm, 0444, show_temp_alarm, NULL, 0),
|
466 |
|
|
SENSOR_ATTR(temp2_input, 0444, show_temp_value, NULL, 1),
|
467 |
|
|
SENSOR_ATTR(temp2_max, 0644, show_temp_max, store_temp_max, 1),
|
468 |
|
|
SENSOR_ATTR(temp2_fault, 0444, show_temp_fault, NULL, 1),
|
469 |
|
|
SENSOR_ATTR(temp2_alarm, 0444, show_temp_alarm, NULL, 1),
|
470 |
|
|
SENSOR_ATTR(temp3_input, 0444, show_temp_value, NULL, 2),
|
471 |
|
|
SENSOR_ATTR(temp3_max, 0644, show_temp_max, store_temp_max, 2),
|
472 |
|
|
SENSOR_ATTR(temp3_fault, 0444, show_temp_fault, NULL, 2),
|
473 |
|
|
SENSOR_ATTR(temp3_alarm, 0444, show_temp_alarm, NULL, 2),
|
474 |
|
|
SENSOR_ATTR(temp4_input, 0444, show_temp_value, NULL, 3),
|
475 |
|
|
SENSOR_ATTR(temp4_max, 0644, show_temp_max, store_temp_max, 3),
|
476 |
|
|
SENSOR_ATTR(temp4_fault, 0444, show_temp_fault, NULL, 3),
|
477 |
|
|
SENSOR_ATTR(temp4_alarm, 0444, show_temp_alarm, NULL, 3),
|
478 |
|
|
SENSOR_ATTR(temp5_input, 0444, show_temp_value, NULL, 4),
|
479 |
|
|
SENSOR_ATTR(temp5_max, 0644, show_temp_max, store_temp_max, 4),
|
480 |
|
|
SENSOR_ATTR(temp5_fault, 0444, show_temp_fault, NULL, 4),
|
481 |
|
|
SENSOR_ATTR(temp5_alarm, 0444, show_temp_alarm, NULL, 4),
|
482 |
|
|
};
|
483 |
|
|
|
484 |
|
|
static struct sensor_device_attribute fschmd_fan_attr[] = {
|
485 |
|
|
SENSOR_ATTR(fan1_input, 0444, show_fan_value, NULL, 0),
|
486 |
|
|
SENSOR_ATTR(fan1_div, 0644, show_fan_div, store_fan_div, 0),
|
487 |
|
|
SENSOR_ATTR(fan1_alarm, 0444, show_fan_alarm, NULL, 0),
|
488 |
|
|
SENSOR_ATTR(fan1_fault, 0444, show_fan_fault, NULL, 0),
|
489 |
|
|
SENSOR_ATTR(pwm1_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
490 |
|
|
store_pwm_auto_point1_pwm, 0),
|
491 |
|
|
SENSOR_ATTR(fan2_input, 0444, show_fan_value, NULL, 1),
|
492 |
|
|
SENSOR_ATTR(fan2_div, 0644, show_fan_div, store_fan_div, 1),
|
493 |
|
|
SENSOR_ATTR(fan2_alarm, 0444, show_fan_alarm, NULL, 1),
|
494 |
|
|
SENSOR_ATTR(fan2_fault, 0444, show_fan_fault, NULL, 1),
|
495 |
|
|
SENSOR_ATTR(pwm2_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
496 |
|
|
store_pwm_auto_point1_pwm, 1),
|
497 |
|
|
SENSOR_ATTR(fan3_input, 0444, show_fan_value, NULL, 2),
|
498 |
|
|
SENSOR_ATTR(fan3_div, 0644, show_fan_div, store_fan_div, 2),
|
499 |
|
|
SENSOR_ATTR(fan3_alarm, 0444, show_fan_alarm, NULL, 2),
|
500 |
|
|
SENSOR_ATTR(fan3_fault, 0444, show_fan_fault, NULL, 2),
|
501 |
|
|
SENSOR_ATTR(pwm3_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
502 |
|
|
store_pwm_auto_point1_pwm, 2),
|
503 |
|
|
SENSOR_ATTR(fan4_input, 0444, show_fan_value, NULL, 3),
|
504 |
|
|
SENSOR_ATTR(fan4_div, 0644, show_fan_div, store_fan_div, 3),
|
505 |
|
|
SENSOR_ATTR(fan4_alarm, 0444, show_fan_alarm, NULL, 3),
|
506 |
|
|
SENSOR_ATTR(fan4_fault, 0444, show_fan_fault, NULL, 3),
|
507 |
|
|
SENSOR_ATTR(pwm4_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
508 |
|
|
store_pwm_auto_point1_pwm, 3),
|
509 |
|
|
SENSOR_ATTR(fan5_input, 0444, show_fan_value, NULL, 4),
|
510 |
|
|
SENSOR_ATTR(fan5_div, 0644, show_fan_div, store_fan_div, 4),
|
511 |
|
|
SENSOR_ATTR(fan5_alarm, 0444, show_fan_alarm, NULL, 4),
|
512 |
|
|
SENSOR_ATTR(fan5_fault, 0444, show_fan_fault, NULL, 4),
|
513 |
|
|
SENSOR_ATTR(pwm5_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
514 |
|
|
store_pwm_auto_point1_pwm, 4),
|
515 |
|
|
SENSOR_ATTR(fan6_input, 0444, show_fan_value, NULL, 5),
|
516 |
|
|
SENSOR_ATTR(fan6_div, 0644, show_fan_div, store_fan_div, 5),
|
517 |
|
|
SENSOR_ATTR(fan6_alarm, 0444, show_fan_alarm, NULL, 5),
|
518 |
|
|
SENSOR_ATTR(fan6_fault, 0444, show_fan_fault, NULL, 5),
|
519 |
|
|
SENSOR_ATTR(pwm6_auto_point1_pwm, 0644, show_pwm_auto_point1_pwm,
|
520 |
|
|
store_pwm_auto_point1_pwm, 5),
|
521 |
|
|
};
|
522 |
|
|
|
523 |
|
|
|
524 |
|
|
/*
|
525 |
|
|
* Real code
|
526 |
|
|
*/
|
527 |
|
|
|
528 |
|
|
static int fschmd_detect(struct i2c_adapter *adapter, int address, int kind)
|
529 |
|
|
{
|
530 |
|
|
struct i2c_client *client;
|
531 |
|
|
struct fschmd_data *data;
|
532 |
|
|
u8 revision;
|
533 |
|
|
const char * const names[5] = { "Poseidon", "Hermes", "Scylla",
|
534 |
|
|
"Heracles", "Heimdall" };
|
535 |
|
|
const char * const client_names[5] = { "fscpos", "fscher", "fscscy",
|
536 |
|
|
"fschrc", "fschmd" };
|
537 |
|
|
int i, err = 0;
|
538 |
|
|
|
539 |
|
|
if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA))
|
540 |
|
|
return 0;
|
541 |
|
|
|
542 |
|
|
/* OK. For now, we presume we have a valid client. We now create the
|
543 |
|
|
* client structure, even though we cannot fill it completely yet.
|
544 |
|
|
* But it allows us to access i2c_smbus_read_byte_data. */
|
545 |
|
|
if (!(data = kzalloc(sizeof(struct fschmd_data), GFP_KERNEL)))
|
546 |
|
|
return -ENOMEM;
|
547 |
|
|
|
548 |
|
|
client = &data->client;
|
549 |
|
|
i2c_set_clientdata(client, data);
|
550 |
|
|
client->addr = address;
|
551 |
|
|
client->adapter = adapter;
|
552 |
|
|
client->driver = &fschmd_driver;
|
553 |
|
|
mutex_init(&data->update_lock);
|
554 |
|
|
|
555 |
|
|
/* Detect & Identify the chip */
|
556 |
|
|
if (kind <= 0) {
|
557 |
|
|
char id[4];
|
558 |
|
|
|
559 |
|
|
id[0] = i2c_smbus_read_byte_data(client,
|
560 |
|
|
FSCHMD_REG_IDENT_0);
|
561 |
|
|
id[1] = i2c_smbus_read_byte_data(client,
|
562 |
|
|
FSCHMD_REG_IDENT_1);
|
563 |
|
|
id[2] = i2c_smbus_read_byte_data(client,
|
564 |
|
|
FSCHMD_REG_IDENT_2);
|
565 |
|
|
id[3] = '\0';
|
566 |
|
|
|
567 |
|
|
if (!strcmp(id, "PEG"))
|
568 |
|
|
kind = fscpos;
|
569 |
|
|
else if (!strcmp(id, "HER"))
|
570 |
|
|
kind = fscher;
|
571 |
|
|
else if (!strcmp(id, "SCY"))
|
572 |
|
|
kind = fscscy;
|
573 |
|
|
else if (!strcmp(id, "HRC"))
|
574 |
|
|
kind = fschrc;
|
575 |
|
|
else if (!strcmp(id, "HMD"))
|
576 |
|
|
kind = fschmd;
|
577 |
|
|
else
|
578 |
|
|
goto exit_free;
|
579 |
|
|
}
|
580 |
|
|
|
581 |
|
|
if (kind == fscpos) {
|
582 |
|
|
/* The Poseidon has hardwired temp limits, fill these
|
583 |
|
|
in for the alarm resetting code */
|
584 |
|
|
data->temp_max[0] = 70 + 128;
|
585 |
|
|
data->temp_max[1] = 50 + 128;
|
586 |
|
|
data->temp_max[2] = 50 + 128;
|
587 |
|
|
}
|
588 |
|
|
|
589 |
|
|
/* i2c kind goes from 1-5, we want from 0-4 to address arrays */
|
590 |
|
|
data->kind = kind - 1;
|
591 |
|
|
strlcpy(client->name, client_names[data->kind], I2C_NAME_SIZE);
|
592 |
|
|
|
593 |
|
|
/* Tell the I2C layer a new client has arrived */
|
594 |
|
|
if ((err = i2c_attach_client(client)))
|
595 |
|
|
goto exit_free;
|
596 |
|
|
|
597 |
|
|
for (i = 0; i < ARRAY_SIZE(fschmd_attr); i++) {
|
598 |
|
|
err = device_create_file(&client->dev,
|
599 |
|
|
&fschmd_attr[i].dev_attr);
|
600 |
|
|
if (err)
|
601 |
|
|
goto exit_detach;
|
602 |
|
|
}
|
603 |
|
|
|
604 |
|
|
for (i = 0; i < (FSCHMD_NO_TEMP_SENSORS[data->kind] * 4); i++) {
|
605 |
|
|
/* Poseidon doesn't have TEMP_LIMIT registers */
|
606 |
|
|
if (kind == fscpos && fschmd_temp_attr[i].dev_attr.show ==
|
607 |
|
|
show_temp_max)
|
608 |
|
|
continue;
|
609 |
|
|
|
610 |
|
|
err = device_create_file(&client->dev,
|
611 |
|
|
&fschmd_temp_attr[i].dev_attr);
|
612 |
|
|
if (err)
|
613 |
|
|
goto exit_detach;
|
614 |
|
|
}
|
615 |
|
|
|
616 |
|
|
for (i = 0; i < (FSCHMD_NO_FAN_SENSORS[data->kind] * 5); i++) {
|
617 |
|
|
/* Poseidon doesn't have a FAN_MIN register for its 3rd fan */
|
618 |
|
|
if (kind == fscpos &&
|
619 |
|
|
!strcmp(fschmd_fan_attr[i].dev_attr.attr.name,
|
620 |
|
|
"pwm3_auto_point1_pwm"))
|
621 |
|
|
continue;
|
622 |
|
|
|
623 |
|
|
err = device_create_file(&client->dev,
|
624 |
|
|
&fschmd_fan_attr[i].dev_attr);
|
625 |
|
|
if (err)
|
626 |
|
|
goto exit_detach;
|
627 |
|
|
}
|
628 |
|
|
|
629 |
|
|
data->hwmon_dev = hwmon_device_register(&client->dev);
|
630 |
|
|
if (IS_ERR(data->hwmon_dev)) {
|
631 |
|
|
err = PTR_ERR(data->hwmon_dev);
|
632 |
|
|
data->hwmon_dev = NULL;
|
633 |
|
|
goto exit_detach;
|
634 |
|
|
}
|
635 |
|
|
|
636 |
|
|
revision = i2c_smbus_read_byte_data(client, FSCHMD_REG_REVISION);
|
637 |
|
|
printk(KERN_INFO FSCHMD_NAME ": Detected FSC %s chip, revision: %d\n",
|
638 |
|
|
names[data->kind], (int) revision);
|
639 |
|
|
|
640 |
|
|
return 0;
|
641 |
|
|
|
642 |
|
|
exit_detach:
|
643 |
|
|
fschmd_detach_client(client); /* will also free data for us */
|
644 |
|
|
return err;
|
645 |
|
|
|
646 |
|
|
exit_free:
|
647 |
|
|
kfree(data);
|
648 |
|
|
return err;
|
649 |
|
|
}
|
650 |
|
|
|
651 |
|
|
static int fschmd_attach_adapter(struct i2c_adapter *adapter)
|
652 |
|
|
{
|
653 |
|
|
if (!(adapter->class & I2C_CLASS_HWMON))
|
654 |
|
|
return 0;
|
655 |
|
|
return i2c_probe(adapter, &addr_data, fschmd_detect);
|
656 |
|
|
}
|
657 |
|
|
|
658 |
|
|
static int fschmd_detach_client(struct i2c_client *client)
|
659 |
|
|
{
|
660 |
|
|
struct fschmd_data *data = i2c_get_clientdata(client);
|
661 |
|
|
int i, err;
|
662 |
|
|
|
663 |
|
|
/* Check if registered in case we're called from fschmd_detect
|
664 |
|
|
to cleanup after an error */
|
665 |
|
|
if (data->hwmon_dev)
|
666 |
|
|
hwmon_device_unregister(data->hwmon_dev);
|
667 |
|
|
|
668 |
|
|
for (i = 0; i < ARRAY_SIZE(fschmd_attr); i++)
|
669 |
|
|
device_remove_file(&client->dev, &fschmd_attr[i].dev_attr);
|
670 |
|
|
for (i = 0; i < (FSCHMD_NO_TEMP_SENSORS[data->kind] * 4); i++)
|
671 |
|
|
device_remove_file(&client->dev,
|
672 |
|
|
&fschmd_temp_attr[i].dev_attr);
|
673 |
|
|
for (i = 0; i < (FSCHMD_NO_FAN_SENSORS[data->kind] * 5); i++)
|
674 |
|
|
device_remove_file(&client->dev,
|
675 |
|
|
&fschmd_fan_attr[i].dev_attr);
|
676 |
|
|
|
677 |
|
|
if ((err = i2c_detach_client(client)))
|
678 |
|
|
return err;
|
679 |
|
|
|
680 |
|
|
kfree(data);
|
681 |
|
|
return 0;
|
682 |
|
|
}
|
683 |
|
|
|
684 |
|
|
static struct fschmd_data *fschmd_update_device(struct device *dev)
|
685 |
|
|
{
|
686 |
|
|
struct i2c_client *client = to_i2c_client(dev);
|
687 |
|
|
struct fschmd_data *data = i2c_get_clientdata(client);
|
688 |
|
|
int i;
|
689 |
|
|
|
690 |
|
|
mutex_lock(&data->update_lock);
|
691 |
|
|
|
692 |
|
|
if (time_after(jiffies, data->last_updated + 2 * HZ) || !data->valid) {
|
693 |
|
|
|
694 |
|
|
for (i = 0; i < FSCHMD_NO_TEMP_SENSORS[data->kind]; i++) {
|
695 |
|
|
data->temp_act[i] = i2c_smbus_read_byte_data(client,
|
696 |
|
|
FSCHMD_REG_TEMP_ACT[data->kind][i]);
|
697 |
|
|
data->temp_status[i] = i2c_smbus_read_byte_data(client,
|
698 |
|
|
FSCHMD_REG_TEMP_STATE[data->kind][i]);
|
699 |
|
|
|
700 |
|
|
/* The fscpos doesn't have TEMP_LIMIT registers */
|
701 |
|
|
if (FSCHMD_REG_TEMP_LIMIT[data->kind][i])
|
702 |
|
|
data->temp_max[i] = i2c_smbus_read_byte_data(
|
703 |
|
|
client,
|
704 |
|
|
FSCHMD_REG_TEMP_LIMIT[data->kind][i]);
|
705 |
|
|
|
706 |
|
|
/* reset alarm if the alarm condition is gone,
|
707 |
|
|
the chip doesn't do this itself */
|
708 |
|
|
if ((data->temp_status[i] & FSCHMD_TEMP_ALARM_MASK) ==
|
709 |
|
|
FSCHMD_TEMP_ALARM_MASK &&
|
710 |
|
|
data->temp_act[i] < data->temp_max[i])
|
711 |
|
|
i2c_smbus_write_byte_data(client,
|
712 |
|
|
FSCHMD_REG_TEMP_STATE[data->kind][i],
|
713 |
|
|
FSCHMD_TEMP_ALERT_MASK);
|
714 |
|
|
}
|
715 |
|
|
|
716 |
|
|
for (i = 0; i < FSCHMD_NO_FAN_SENSORS[data->kind]; i++) {
|
717 |
|
|
data->fan_act[i] = i2c_smbus_read_byte_data(client,
|
718 |
|
|
FSCHMD_REG_FAN_ACT[data->kind][i]);
|
719 |
|
|
data->fan_status[i] = i2c_smbus_read_byte_data(client,
|
720 |
|
|
FSCHMD_REG_FAN_STATE[data->kind][i]);
|
721 |
|
|
data->fan_ripple[i] = i2c_smbus_read_byte_data(client,
|
722 |
|
|
FSCHMD_REG_FAN_RIPPLE[data->kind][i]);
|
723 |
|
|
|
724 |
|
|
/* The fscpos third fan doesn't have a fan_min */
|
725 |
|
|
if (FSCHMD_REG_FAN_MIN[data->kind][i])
|
726 |
|
|
data->fan_min[i] = i2c_smbus_read_byte_data(
|
727 |
|
|
client,
|
728 |
|
|
FSCHMD_REG_FAN_MIN[data->kind][i]);
|
729 |
|
|
|
730 |
|
|
/* reset fan status if speed is back to > 0 */
|
731 |
|
|
if ((data->fan_status[i] & FSCHMD_FAN_ALARM_MASK) &&
|
732 |
|
|
data->fan_act[i])
|
733 |
|
|
i2c_smbus_write_byte_data(client,
|
734 |
|
|
FSCHMD_REG_FAN_STATE[data->kind][i],
|
735 |
|
|
FSCHMD_FAN_ALARM_MASK);
|
736 |
|
|
}
|
737 |
|
|
|
738 |
|
|
for (i = 0; i < 3; i++)
|
739 |
|
|
data->volt[i] = i2c_smbus_read_byte_data(client,
|
740 |
|
|
FSCHMD_REG_VOLT[i]);
|
741 |
|
|
|
742 |
|
|
data->global_control = i2c_smbus_read_byte_data(client,
|
743 |
|
|
FSCHMD_REG_CONTROL);
|
744 |
|
|
|
745 |
|
|
/* To be implemented in the future
|
746 |
|
|
data->watchdog[0] = i2c_smbus_read_byte_data(client,
|
747 |
|
|
FSCHMD_REG_WDOG_PRESET);
|
748 |
|
|
data->watchdog[1] = i2c_smbus_read_byte_data(client,
|
749 |
|
|
FSCHMD_REG_WDOG_STATE);
|
750 |
|
|
data->watchdog[2] = i2c_smbus_read_byte_data(client,
|
751 |
|
|
FSCHMD_REG_WDOG_CONTROL); */
|
752 |
|
|
|
753 |
|
|
data->last_updated = jiffies;
|
754 |
|
|
data->valid = 1;
|
755 |
|
|
}
|
756 |
|
|
|
757 |
|
|
mutex_unlock(&data->update_lock);
|
758 |
|
|
|
759 |
|
|
return data;
|
760 |
|
|
}
|
761 |
|
|
|
762 |
|
|
static int __init fschmd_init(void)
|
763 |
|
|
{
|
764 |
|
|
return i2c_add_driver(&fschmd_driver);
|
765 |
|
|
}
|
766 |
|
|
|
767 |
|
|
static void __exit fschmd_exit(void)
|
768 |
|
|
{
|
769 |
|
|
i2c_del_driver(&fschmd_driver);
|
770 |
|
|
}
|
771 |
|
|
|
772 |
|
|
MODULE_AUTHOR("Hans de Goede <j.w.r.degoede@hhs.nl>");
|
773 |
|
|
MODULE_DESCRIPTION("FSC Poseidon, Hermes, Scylla, Heracles and "
|
774 |
|
|
"Heimdall driver");
|
775 |
|
|
MODULE_LICENSE("GPL");
|
776 |
|
|
|
777 |
|
|
module_init(fschmd_init);
|
778 |
|
|
module_exit(fschmd_exit);
|