1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | |
3 | /* |
4 | * LED & force feedback support for BigBen Interactive |
5 | * |
6 | * 0x146b:0x0902 "Bigben Interactive Bigben Game Pad" |
7 | * "Kid-friendly Wired Controller" PS3OFMINIPAD SONY |
8 | * sold for use with the PS3 |
9 | * |
10 | * Copyright (c) 2018 Hanno Zulla <kontakt@hanno.de> |
11 | */ |
12 | |
13 | #include <linux/input.h> |
14 | #include <linux/slab.h> |
15 | #include <linux/module.h> |
16 | #include <linux/leds.h> |
17 | #include <linux/hid.h> |
18 | |
19 | #include "hid-ids.h" |
20 | |
21 | |
22 | /* |
23 | * The original descriptor for 0x146b:0x0902 |
24 | * |
25 | * 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) |
26 | * 0x09, 0x05, // Usage (Game Pad) |
27 | * 0xA1, 0x01, // Collection (Application) |
28 | * 0x15, 0x00, // Logical Minimum (0) |
29 | * 0x25, 0x01, // Logical Maximum (1) |
30 | * 0x35, 0x00, // Physical Minimum (0) |
31 | * 0x45, 0x01, // Physical Maximum (1) |
32 | * 0x75, 0x01, // Report Size (1) |
33 | * 0x95, 0x0D, // Report Count (13) |
34 | * 0x05, 0x09, // Usage Page (Button) |
35 | * 0x19, 0x01, // Usage Minimum (0x01) |
36 | * 0x29, 0x0D, // Usage Maximum (0x0D) |
37 | * 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) |
38 | * 0x95, 0x03, // Report Count (3) |
39 | * 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) |
40 | * 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) |
41 | * 0x25, 0x07, // Logical Maximum (7) |
42 | * 0x46, 0x3B, 0x01, // Physical Maximum (315) |
43 | * 0x75, 0x04, // Report Size (4) |
44 | * 0x95, 0x01, // Report Count (1) |
45 | * 0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter) |
46 | * 0x09, 0x39, // Usage (Hat switch) |
47 | * 0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) |
48 | * 0x65, 0x00, // Unit (None) |
49 | * 0x95, 0x01, // Report Count (1) |
50 | * 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) |
51 | * 0x26, 0xFF, 0x00, // Logical Maximum (255) |
52 | * 0x46, 0xFF, 0x00, // Physical Maximum (255) |
53 | * 0x09, 0x30, // Usage (X) |
54 | * 0x09, 0x31, // Usage (Y) |
55 | * 0x09, 0x32, // Usage (Z) |
56 | * 0x09, 0x35, // Usage (Rz) |
57 | * 0x75, 0x08, // Report Size (8) |
58 | * 0x95, 0x04, // Report Count (4) |
59 | * 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) |
60 | * 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) |
61 | * 0x09, 0x20, // Usage (0x20) |
62 | * 0x09, 0x21, // Usage (0x21) |
63 | * 0x09, 0x22, // Usage (0x22) |
64 | * 0x09, 0x23, // Usage (0x23) |
65 | * 0x09, 0x24, // Usage (0x24) |
66 | * 0x09, 0x25, // Usage (0x25) |
67 | * 0x09, 0x26, // Usage (0x26) |
68 | * 0x09, 0x27, // Usage (0x27) |
69 | * 0x09, 0x28, // Usage (0x28) |
70 | * 0x09, 0x29, // Usage (0x29) |
71 | * 0x09, 0x2A, // Usage (0x2A) |
72 | * 0x09, 0x2B, // Usage (0x2B) |
73 | * 0x95, 0x0C, // Report Count (12) |
74 | * 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) |
75 | * 0x0A, 0x21, 0x26, // Usage (0x2621) |
76 | * 0x95, 0x08, // Report Count (8) |
77 | * 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) |
78 | * 0x0A, 0x21, 0x26, // Usage (0x2621) |
79 | * 0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) |
80 | * 0x26, 0xFF, 0x03, // Logical Maximum (1023) |
81 | * 0x46, 0xFF, 0x03, // Physical Maximum (1023) |
82 | * 0x09, 0x2C, // Usage (0x2C) |
83 | * 0x09, 0x2D, // Usage (0x2D) |
84 | * 0x09, 0x2E, // Usage (0x2E) |
85 | * 0x09, 0x2F, // Usage (0x2F) |
86 | * 0x75, 0x10, // Report Size (16) |
87 | * 0x95, 0x04, // Report Count (4) |
88 | * 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) |
89 | * 0xC0, // End Collection |
90 | */ |
91 | |
92 | #define PID0902_RDESC_ORIG_SIZE 137 |
93 | |
94 | /* |
95 | * The fixed descriptor for 0x146b:0x0902 |
96 | * |
97 | * - map buttons according to gamepad.rst |
98 | * - assign right stick from Z/Rz to Rx/Ry |
99 | * - map previously unused analog trigger data to Z/RZ |
100 | * - simplify feature and output descriptor |
101 | */ |
102 | static __u8 pid0902_rdesc_fixed[] = { |
103 | 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ |
104 | 0x09, 0x05, /* Usage (Game Pad) */ |
105 | 0xA1, 0x01, /* Collection (Application) */ |
106 | 0x15, 0x00, /* Logical Minimum (0) */ |
107 | 0x25, 0x01, /* Logical Maximum (1) */ |
108 | 0x35, 0x00, /* Physical Minimum (0) */ |
109 | 0x45, 0x01, /* Physical Maximum (1) */ |
110 | 0x75, 0x01, /* Report Size (1) */ |
111 | 0x95, 0x0D, /* Report Count (13) */ |
112 | 0x05, 0x09, /* Usage Page (Button) */ |
113 | 0x09, 0x05, /* Usage (BTN_WEST) */ |
114 | 0x09, 0x01, /* Usage (BTN_SOUTH) */ |
115 | 0x09, 0x02, /* Usage (BTN_EAST) */ |
116 | 0x09, 0x04, /* Usage (BTN_NORTH) */ |
117 | 0x09, 0x07, /* Usage (BTN_TL) */ |
118 | 0x09, 0x08, /* Usage (BTN_TR) */ |
119 | 0x09, 0x09, /* Usage (BTN_TL2) */ |
120 | 0x09, 0x0A, /* Usage (BTN_TR2) */ |
121 | 0x09, 0x0B, /* Usage (BTN_SELECT) */ |
122 | 0x09, 0x0C, /* Usage (BTN_START) */ |
123 | 0x09, 0x0E, /* Usage (BTN_THUMBL) */ |
124 | 0x09, 0x0F, /* Usage (BTN_THUMBR) */ |
125 | 0x09, 0x0D, /* Usage (BTN_MODE) */ |
126 | 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
127 | 0x75, 0x01, /* Report Size (1) */ |
128 | 0x95, 0x03, /* Report Count (3) */ |
129 | 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
130 | 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ |
131 | 0x25, 0x07, /* Logical Maximum (7) */ |
132 | 0x46, 0x3B, 0x01, /* Physical Maximum (315) */ |
133 | 0x75, 0x04, /* Report Size (4) */ |
134 | 0x95, 0x01, /* Report Count (1) */ |
135 | 0x65, 0x14, /* Unit (System: English Rotation, Length: Centimeter) */ |
136 | 0x09, 0x39, /* Usage (Hat switch) */ |
137 | 0x81, 0x42, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State) */ |
138 | 0x65, 0x00, /* Unit (None) */ |
139 | 0x95, 0x01, /* Report Count (1) */ |
140 | 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
141 | 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ |
142 | 0x46, 0xFF, 0x00, /* Physical Maximum (255) */ |
143 | 0x09, 0x30, /* Usage (X) */ |
144 | 0x09, 0x31, /* Usage (Y) */ |
145 | 0x09, 0x33, /* Usage (Rx) */ |
146 | 0x09, 0x34, /* Usage (Ry) */ |
147 | 0x75, 0x08, /* Report Size (8) */ |
148 | 0x95, 0x04, /* Report Count (4) */ |
149 | 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
150 | 0x95, 0x0A, /* Report Count (10) */ |
151 | 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
152 | 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ |
153 | 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ |
154 | 0x46, 0xFF, 0x00, /* Physical Maximum (255) */ |
155 | 0x09, 0x32, /* Usage (Z) */ |
156 | 0x09, 0x35, /* Usage (Rz) */ |
157 | 0x95, 0x02, /* Report Count (2) */ |
158 | 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
159 | 0x95, 0x08, /* Report Count (8) */ |
160 | 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
161 | 0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */ |
162 | 0xB1, 0x02, /* Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */ |
163 | 0x0A, 0x21, 0x26, /* Usage (0x2621) */ |
164 | 0x95, 0x08, /* Report Count (8) */ |
165 | 0x91, 0x02, /* Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */ |
166 | 0x0A, 0x21, 0x26, /* Usage (0x2621) */ |
167 | 0x95, 0x08, /* Report Count (8) */ |
168 | 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ |
169 | 0xC0, /* End Collection */ |
170 | }; |
171 | |
172 | #define NUM_LEDS 4 |
173 | |
174 | struct bigben_device { |
175 | struct hid_device *hid; |
176 | struct hid_report *report; |
177 | spinlock_t lock; |
178 | bool removed; |
179 | u8 led_state; /* LED1 = 1 .. LED4 = 8 */ |
180 | u8 right_motor_on; /* right motor off/on 0/1 */ |
181 | u8 left_motor_force; /* left motor force 0-255 */ |
182 | struct led_classdev *leds[NUM_LEDS]; |
183 | bool work_led; |
184 | bool work_ff; |
185 | struct work_struct worker; |
186 | }; |
187 | |
188 | static inline void bigben_schedule_work(struct bigben_device *bigben) |
189 | { |
190 | unsigned long flags; |
191 | |
192 | spin_lock_irqsave(&bigben->lock, flags); |
193 | if (!bigben->removed) |
194 | schedule_work(work: &bigben->worker); |
195 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
196 | } |
197 | |
198 | static void bigben_worker(struct work_struct *work) |
199 | { |
200 | struct bigben_device *bigben = container_of(work, |
201 | struct bigben_device, worker); |
202 | struct hid_field *report_field = bigben->report->field[0]; |
203 | bool do_work_led = false; |
204 | bool do_work_ff = false; |
205 | u8 *buf; |
206 | u32 len; |
207 | unsigned long flags; |
208 | |
209 | buf = hid_alloc_report_buf(report: bigben->report, GFP_KERNEL); |
210 | if (!buf) |
211 | return; |
212 | |
213 | len = hid_report_len(report: bigben->report); |
214 | |
215 | /* LED work */ |
216 | spin_lock_irqsave(&bigben->lock, flags); |
217 | |
218 | if (bigben->work_led) { |
219 | bigben->work_led = false; |
220 | do_work_led = true; |
221 | report_field->value[0] = 0x01; /* 1 = led message */ |
222 | report_field->value[1] = 0x08; /* reserved value, always 8 */ |
223 | report_field->value[2] = bigben->led_state; |
224 | report_field->value[3] = 0x00; /* padding */ |
225 | report_field->value[4] = 0x00; /* padding */ |
226 | report_field->value[5] = 0x00; /* padding */ |
227 | report_field->value[6] = 0x00; /* padding */ |
228 | report_field->value[7] = 0x00; /* padding */ |
229 | hid_output_report(report: bigben->report, data: buf); |
230 | } |
231 | |
232 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
233 | |
234 | if (do_work_led) { |
235 | hid_hw_raw_request(hdev: bigben->hid, reportnum: bigben->report->id, buf, len, |
236 | rtype: bigben->report->type, reqtype: HID_REQ_SET_REPORT); |
237 | } |
238 | |
239 | /* FF work */ |
240 | spin_lock_irqsave(&bigben->lock, flags); |
241 | |
242 | if (bigben->work_ff) { |
243 | bigben->work_ff = false; |
244 | do_work_ff = true; |
245 | report_field->value[0] = 0x02; /* 2 = rumble effect message */ |
246 | report_field->value[1] = 0x08; /* reserved value, always 8 */ |
247 | report_field->value[2] = bigben->right_motor_on; |
248 | report_field->value[3] = bigben->left_motor_force; |
249 | report_field->value[4] = 0xff; /* duration 0-254 (255 = nonstop) */ |
250 | report_field->value[5] = 0x00; /* padding */ |
251 | report_field->value[6] = 0x00; /* padding */ |
252 | report_field->value[7] = 0x00; /* padding */ |
253 | hid_output_report(report: bigben->report, data: buf); |
254 | } |
255 | |
256 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
257 | |
258 | if (do_work_ff) { |
259 | hid_hw_raw_request(hdev: bigben->hid, reportnum: bigben->report->id, buf, len, |
260 | rtype: bigben->report->type, reqtype: HID_REQ_SET_REPORT); |
261 | } |
262 | |
263 | kfree(objp: buf); |
264 | } |
265 | |
266 | static int hid_bigben_play_effect(struct input_dev *dev, void *data, |
267 | struct ff_effect *effect) |
268 | { |
269 | struct hid_device *hid = input_get_drvdata(dev); |
270 | struct bigben_device *bigben = hid_get_drvdata(hdev: hid); |
271 | u8 right_motor_on; |
272 | u8 left_motor_force; |
273 | unsigned long flags; |
274 | |
275 | if (!bigben) { |
276 | hid_err(hid, "no device data\n" ); |
277 | return 0; |
278 | } |
279 | |
280 | if (effect->type != FF_RUMBLE) |
281 | return 0; |
282 | |
283 | right_motor_on = effect->u.rumble.weak_magnitude ? 1 : 0; |
284 | left_motor_force = effect->u.rumble.strong_magnitude / 256; |
285 | |
286 | if (right_motor_on != bigben->right_motor_on || |
287 | left_motor_force != bigben->left_motor_force) { |
288 | spin_lock_irqsave(&bigben->lock, flags); |
289 | bigben->right_motor_on = right_motor_on; |
290 | bigben->left_motor_force = left_motor_force; |
291 | bigben->work_ff = true; |
292 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
293 | |
294 | bigben_schedule_work(bigben); |
295 | } |
296 | |
297 | return 0; |
298 | } |
299 | |
300 | static void bigben_set_led(struct led_classdev *led, |
301 | enum led_brightness value) |
302 | { |
303 | struct device *dev = led->dev->parent; |
304 | struct hid_device *hid = to_hid_device(dev); |
305 | struct bigben_device *bigben = hid_get_drvdata(hdev: hid); |
306 | int n; |
307 | bool work; |
308 | unsigned long flags; |
309 | |
310 | if (!bigben) { |
311 | hid_err(hid, "no device data\n" ); |
312 | return; |
313 | } |
314 | |
315 | for (n = 0; n < NUM_LEDS; n++) { |
316 | if (led == bigben->leds[n]) { |
317 | spin_lock_irqsave(&bigben->lock, flags); |
318 | if (value == LED_OFF) { |
319 | work = (bigben->led_state & BIT(n)); |
320 | bigben->led_state &= ~BIT(n); |
321 | } else { |
322 | work = !(bigben->led_state & BIT(n)); |
323 | bigben->led_state |= BIT(n); |
324 | } |
325 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
326 | |
327 | if (work) { |
328 | bigben->work_led = true; |
329 | bigben_schedule_work(bigben); |
330 | } |
331 | return; |
332 | } |
333 | } |
334 | } |
335 | |
336 | static enum led_brightness bigben_get_led(struct led_classdev *led) |
337 | { |
338 | struct device *dev = led->dev->parent; |
339 | struct hid_device *hid = to_hid_device(dev); |
340 | struct bigben_device *bigben = hid_get_drvdata(hdev: hid); |
341 | int n; |
342 | |
343 | if (!bigben) { |
344 | hid_err(hid, "no device data\n" ); |
345 | return LED_OFF; |
346 | } |
347 | |
348 | for (n = 0; n < NUM_LEDS; n++) { |
349 | if (led == bigben->leds[n]) |
350 | return (bigben->led_state & BIT(n)) ? LED_ON : LED_OFF; |
351 | } |
352 | |
353 | return LED_OFF; |
354 | } |
355 | |
356 | static void bigben_remove(struct hid_device *hid) |
357 | { |
358 | struct bigben_device *bigben = hid_get_drvdata(hdev: hid); |
359 | unsigned long flags; |
360 | |
361 | spin_lock_irqsave(&bigben->lock, flags); |
362 | bigben->removed = true; |
363 | spin_unlock_irqrestore(lock: &bigben->lock, flags); |
364 | |
365 | cancel_work_sync(work: &bigben->worker); |
366 | hid_hw_stop(hdev: hid); |
367 | } |
368 | |
369 | static int bigben_probe(struct hid_device *hid, |
370 | const struct hid_device_id *id) |
371 | { |
372 | struct bigben_device *bigben; |
373 | struct hid_input *hidinput; |
374 | struct led_classdev *led; |
375 | char *name; |
376 | size_t name_sz; |
377 | int n, error; |
378 | |
379 | bigben = devm_kzalloc(dev: &hid->dev, size: sizeof(*bigben), GFP_KERNEL); |
380 | if (!bigben) |
381 | return -ENOMEM; |
382 | hid_set_drvdata(hdev: hid, data: bigben); |
383 | bigben->hid = hid; |
384 | bigben->removed = false; |
385 | |
386 | error = hid_parse(hdev: hid); |
387 | if (error) { |
388 | hid_err(hid, "parse failed\n" ); |
389 | return error; |
390 | } |
391 | |
392 | error = hid_hw_start(hdev: hid, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF); |
393 | if (error) { |
394 | hid_err(hid, "hw start failed\n" ); |
395 | return error; |
396 | } |
397 | |
398 | bigben->report = hid_validate_values(hid, type: HID_OUTPUT_REPORT, id: 0, field_index: 0, report_counts: 8); |
399 | if (!bigben->report) { |
400 | hid_err(hid, "no output report found\n" ); |
401 | error = -ENODEV; |
402 | goto error_hw_stop; |
403 | } |
404 | |
405 | if (list_empty(head: &hid->inputs)) { |
406 | hid_err(hid, "no inputs found\n" ); |
407 | error = -ENODEV; |
408 | goto error_hw_stop; |
409 | } |
410 | |
411 | hidinput = list_first_entry(&hid->inputs, struct hid_input, list); |
412 | set_bit(FF_RUMBLE, addr: hidinput->input->ffbit); |
413 | |
414 | INIT_WORK(&bigben->worker, bigben_worker); |
415 | spin_lock_init(&bigben->lock); |
416 | |
417 | error = input_ff_create_memless(dev: hidinput->input, NULL, |
418 | play_effect: hid_bigben_play_effect); |
419 | if (error) |
420 | goto error_hw_stop; |
421 | |
422 | name_sz = strlen(dev_name(&hid->dev)) + strlen(":red:bigben#" ) + 1; |
423 | |
424 | for (n = 0; n < NUM_LEDS; n++) { |
425 | led = devm_kzalloc( |
426 | dev: &hid->dev, |
427 | size: sizeof(struct led_classdev) + name_sz, |
428 | GFP_KERNEL |
429 | ); |
430 | if (!led) { |
431 | error = -ENOMEM; |
432 | goto error_hw_stop; |
433 | } |
434 | name = (void *)(&led[1]); |
435 | snprintf(buf: name, size: name_sz, |
436 | fmt: "%s:red:bigben%d" , |
437 | dev_name(dev: &hid->dev), n + 1 |
438 | ); |
439 | led->name = name; |
440 | led->brightness = (n == 0) ? LED_ON : LED_OFF; |
441 | led->max_brightness = 1; |
442 | led->brightness_get = bigben_get_led; |
443 | led->brightness_set = bigben_set_led; |
444 | bigben->leds[n] = led; |
445 | error = devm_led_classdev_register(parent: &hid->dev, led_cdev: led); |
446 | if (error) |
447 | goto error_hw_stop; |
448 | } |
449 | |
450 | /* initial state: LED1 is on, no rumble effect */ |
451 | bigben->led_state = BIT(0); |
452 | bigben->right_motor_on = 0; |
453 | bigben->left_motor_force = 0; |
454 | bigben->work_led = true; |
455 | bigben->work_ff = true; |
456 | bigben_schedule_work(bigben); |
457 | |
458 | hid_info(hid, "LED and force feedback support for BigBen gamepad\n" ); |
459 | |
460 | return 0; |
461 | |
462 | error_hw_stop: |
463 | hid_hw_stop(hdev: hid); |
464 | return error; |
465 | } |
466 | |
467 | static __u8 *bigben_report_fixup(struct hid_device *hid, __u8 *rdesc, |
468 | unsigned int *rsize) |
469 | { |
470 | if (*rsize == PID0902_RDESC_ORIG_SIZE) { |
471 | rdesc = pid0902_rdesc_fixed; |
472 | *rsize = sizeof(pid0902_rdesc_fixed); |
473 | } else |
474 | hid_warn(hid, "unexpected rdesc, please submit for review\n" ); |
475 | return rdesc; |
476 | } |
477 | |
478 | static const struct hid_device_id bigben_devices[] = { |
479 | { HID_USB_DEVICE(USB_VENDOR_ID_BIGBEN, USB_DEVICE_ID_BIGBEN_PS3OFMINIPAD) }, |
480 | { } |
481 | }; |
482 | MODULE_DEVICE_TABLE(hid, bigben_devices); |
483 | |
484 | static struct hid_driver bigben_driver = { |
485 | .name = "bigben" , |
486 | .id_table = bigben_devices, |
487 | .probe = bigben_probe, |
488 | .report_fixup = bigben_report_fixup, |
489 | .remove = bigben_remove, |
490 | }; |
491 | module_hid_driver(bigben_driver); |
492 | |
493 | MODULE_LICENSE("GPL" ); |
494 | |