]>
Commit | Line | Data |
---|---|---|
e7c256fb BR |
1 | /* |
2 | * cros_ec_dev - expose the Chrome OS Embedded Controller to user-space | |
3 | * | |
4 | * Copyright (C) 2014 Google, Inc. | |
5 | * | |
6 | * This program is free software; you can redistribute it and/or modify | |
7 | * it under the terms of the GNU General Public License as published by | |
8 | * the Free Software Foundation; either version 2 of the License, or | |
9 | * (at your option) any later version. | |
10 | * | |
11 | * This program is distributed in the hope that it will be useful, | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | * GNU General Public License for more details. | |
15 | * | |
16 | * You should have received a copy of the GNU General Public License | |
17 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | */ | |
19 | ||
20 | #include <linux/fs.h> | |
5668bfdd | 21 | #include <linux/mfd/core.h> |
e7c256fb BR |
22 | #include <linux/module.h> |
23 | #include <linux/platform_device.h> | |
405c8430 | 24 | #include <linux/pm.h> |
a8411784 | 25 | #include <linux/slab.h> |
e7c256fb BR |
26 | #include <linux/uaccess.h> |
27 | ||
e8626459 | 28 | #include "cros_ec_debugfs.h" |
e7c256fb BR |
29 | #include "cros_ec_dev.h" |
30 | ||
31 | /* Device variables */ | |
32 | #define CROS_MAX_DEV 128 | |
e7c256fb BR |
33 | static int ec_major; |
34 | ||
57b33ff0 GG |
35 | static const struct attribute_group *cros_ec_groups[] = { |
36 | &cros_ec_attr_group, | |
37 | &cros_ec_lightbar_attr_group, | |
18800fc7 | 38 | &cros_ec_vbc_attr_group, |
57b33ff0 GG |
39 | NULL, |
40 | }; | |
41 | ||
42 | static struct class cros_class = { | |
43 | .owner = THIS_MODULE, | |
44 | .name = "chromeos", | |
45 | .dev_groups = cros_ec_groups, | |
46 | }; | |
47 | ||
e7c256fb | 48 | /* Basic communication */ |
57b33ff0 | 49 | static int ec_get_version(struct cros_ec_dev *ec, char *str, int maxlen) |
e7c256fb BR |
50 | { |
51 | struct ec_response_get_version *resp; | |
52 | static const char * const current_image_name[] = { | |
53 | "unknown", "read-only", "read-write", "invalid", | |
54 | }; | |
a8411784 | 55 | struct cros_ec_command *msg; |
e7c256fb BR |
56 | int ret; |
57 | ||
a8411784 JMC |
58 | msg = kmalloc(sizeof(*msg) + sizeof(*resp), GFP_KERNEL); |
59 | if (!msg) | |
60 | return -ENOMEM; | |
61 | ||
62 | msg->version = 0; | |
57b33ff0 | 63 | msg->command = EC_CMD_GET_VERSION + ec->cmd_offset; |
a8411784 JMC |
64 | msg->insize = sizeof(*resp); |
65 | msg->outsize = 0; | |
66 | ||
57b33ff0 | 67 | ret = cros_ec_cmd_xfer(ec->ec_dev, msg); |
e7c256fb | 68 | if (ret < 0) |
a8411784 | 69 | goto exit; |
e7c256fb | 70 | |
a8411784 | 71 | if (msg->result != EC_RES_SUCCESS) { |
e7c256fb BR |
72 | snprintf(str, maxlen, |
73 | "%s\nUnknown EC version: EC returned %d\n", | |
a8411784 JMC |
74 | CROS_EC_DEV_VERSION, msg->result); |
75 | ret = -EINVAL; | |
76 | goto exit; | |
e7c256fb BR |
77 | } |
78 | ||
a8411784 | 79 | resp = (struct ec_response_get_version *)msg->data; |
e7c256fb BR |
80 | if (resp->current_image >= ARRAY_SIZE(current_image_name)) |
81 | resp->current_image = 3; /* invalid */ | |
82 | ||
ef59c25d | 83 | snprintf(str, maxlen, "%s\n%s\n%s\n%s\n", CROS_EC_DEV_VERSION, |
e7c256fb BR |
84 | resp->version_string_ro, resp->version_string_rw, |
85 | current_image_name[resp->current_image]); | |
86 | ||
a8411784 JMC |
87 | ret = 0; |
88 | exit: | |
89 | kfree(msg); | |
90 | return ret; | |
e7c256fb BR |
91 | } |
92 | ||
e4244ebd VP |
93 | static int cros_ec_check_features(struct cros_ec_dev *ec, int feature) |
94 | { | |
95 | struct cros_ec_command *msg; | |
96 | int ret; | |
97 | ||
98 | if (ec->features[0] == -1U && ec->features[1] == -1U) { | |
99 | /* features bitmap not read yet */ | |
100 | ||
101 | msg = kmalloc(sizeof(*msg) + sizeof(ec->features), GFP_KERNEL); | |
102 | if (!msg) | |
103 | return -ENOMEM; | |
104 | ||
105 | msg->version = 0; | |
106 | msg->command = EC_CMD_GET_FEATURES + ec->cmd_offset; | |
107 | msg->insize = sizeof(ec->features); | |
108 | msg->outsize = 0; | |
109 | ||
110 | ret = cros_ec_cmd_xfer(ec->ec_dev, msg); | |
111 | if (ret < 0 || msg->result != EC_RES_SUCCESS) { | |
112 | dev_warn(ec->dev, "cannot get EC features: %d/%d\n", | |
113 | ret, msg->result); | |
114 | memset(ec->features, 0, sizeof(ec->features)); | |
115 | } | |
116 | ||
117 | memcpy(ec->features, msg->data, sizeof(ec->features)); | |
118 | ||
119 | dev_dbg(ec->dev, "EC features %08x %08x\n", | |
120 | ec->features[0], ec->features[1]); | |
121 | ||
122 | kfree(msg); | |
123 | } | |
124 | ||
125 | return ec->features[feature / 32] & EC_FEATURE_MASK_0(feature); | |
126 | } | |
127 | ||
e7c256fb BR |
128 | /* Device file ops */ |
129 | static int ec_device_open(struct inode *inode, struct file *filp) | |
130 | { | |
57b33ff0 GG |
131 | struct cros_ec_dev *ec = container_of(inode->i_cdev, |
132 | struct cros_ec_dev, cdev); | |
133 | filp->private_data = ec; | |
134 | nonseekable_open(inode, filp); | |
e7c256fb BR |
135 | return 0; |
136 | } | |
137 | ||
138 | static int ec_device_release(struct inode *inode, struct file *filp) | |
139 | { | |
140 | return 0; | |
141 | } | |
142 | ||
143 | static ssize_t ec_device_read(struct file *filp, char __user *buffer, | |
144 | size_t length, loff_t *offset) | |
145 | { | |
57b33ff0 | 146 | struct cros_ec_dev *ec = filp->private_data; |
e7c256fb BR |
147 | char msg[sizeof(struct ec_response_get_version) + |
148 | sizeof(CROS_EC_DEV_VERSION)]; | |
149 | size_t count; | |
150 | int ret; | |
151 | ||
152 | if (*offset != 0) | |
153 | return 0; | |
154 | ||
155 | ret = ec_get_version(ec, msg, sizeof(msg)); | |
156 | if (ret) | |
157 | return ret; | |
158 | ||
159 | count = min(length, strlen(msg)); | |
160 | ||
161 | if (copy_to_user(buffer, msg, count)) | |
162 | return -EFAULT; | |
163 | ||
164 | *offset = count; | |
165 | return count; | |
166 | } | |
167 | ||
168 | /* Ioctls */ | |
57b33ff0 | 169 | static long ec_device_ioctl_xcmd(struct cros_ec_dev *ec, void __user *arg) |
e7c256fb BR |
170 | { |
171 | long ret; | |
a8411784 JMC |
172 | struct cros_ec_command u_cmd; |
173 | struct cros_ec_command *s_cmd; | |
e7c256fb | 174 | |
a8411784 | 175 | if (copy_from_user(&u_cmd, arg, sizeof(u_cmd))) |
e7c256fb BR |
176 | return -EFAULT; |
177 | ||
5d749d0b GG |
178 | if ((u_cmd.outsize > EC_MAX_MSG_BYTES) || |
179 | (u_cmd.insize > EC_MAX_MSG_BYTES)) | |
180 | return -EINVAL; | |
181 | ||
a8411784 JMC |
182 | s_cmd = kmalloc(sizeof(*s_cmd) + max(u_cmd.outsize, u_cmd.insize), |
183 | GFP_KERNEL); | |
184 | if (!s_cmd) | |
185 | return -ENOMEM; | |
186 | ||
187 | if (copy_from_user(s_cmd, arg, sizeof(*s_cmd) + u_cmd.outsize)) { | |
188 | ret = -EFAULT; | |
189 | goto exit; | |
190 | } | |
191 | ||
096cdc6f DC |
192 | if (u_cmd.outsize != s_cmd->outsize || |
193 | u_cmd.insize != s_cmd->insize) { | |
194 | ret = -EINVAL; | |
195 | goto exit; | |
196 | } | |
197 | ||
57b33ff0 GG |
198 | s_cmd->command += ec->cmd_offset; |
199 | ret = cros_ec_cmd_xfer(ec->ec_dev, s_cmd); | |
e7c256fb BR |
200 | /* Only copy data to userland if data was received. */ |
201 | if (ret < 0) | |
a8411784 | 202 | goto exit; |
e7c256fb | 203 | |
096cdc6f | 204 | if (copy_to_user(arg, s_cmd, sizeof(*s_cmd) + s_cmd->insize)) |
a8411784 JMC |
205 | ret = -EFAULT; |
206 | exit: | |
207 | kfree(s_cmd); | |
208 | return ret; | |
e7c256fb BR |
209 | } |
210 | ||
57b33ff0 | 211 | static long ec_device_ioctl_readmem(struct cros_ec_dev *ec, void __user *arg) |
e7c256fb | 212 | { |
57b33ff0 | 213 | struct cros_ec_device *ec_dev = ec->ec_dev; |
e7c256fb BR |
214 | struct cros_ec_readmem s_mem = { }; |
215 | long num; | |
216 | ||
217 | /* Not every platform supports direct reads */ | |
57b33ff0 | 218 | if (!ec_dev->cmd_readmem) |
e7c256fb BR |
219 | return -ENOTTY; |
220 | ||
221 | if (copy_from_user(&s_mem, arg, sizeof(s_mem))) | |
222 | return -EFAULT; | |
223 | ||
57b33ff0 GG |
224 | num = ec_dev->cmd_readmem(ec_dev, s_mem.offset, s_mem.bytes, |
225 | s_mem.buffer); | |
e7c256fb BR |
226 | if (num <= 0) |
227 | return num; | |
228 | ||
229 | if (copy_to_user((void __user *)arg, &s_mem, sizeof(s_mem))) | |
230 | return -EFAULT; | |
231 | ||
232 | return 0; | |
233 | } | |
234 | ||
235 | static long ec_device_ioctl(struct file *filp, unsigned int cmd, | |
236 | unsigned long arg) | |
237 | { | |
57b33ff0 | 238 | struct cros_ec_dev *ec = filp->private_data; |
e7c256fb BR |
239 | |
240 | if (_IOC_TYPE(cmd) != CROS_EC_DEV_IOC) | |
241 | return -ENOTTY; | |
242 | ||
243 | switch (cmd) { | |
244 | case CROS_EC_DEV_IOCXCMD: | |
245 | return ec_device_ioctl_xcmd(ec, (void __user *)arg); | |
246 | case CROS_EC_DEV_IOCRDMEM: | |
247 | return ec_device_ioctl_readmem(ec, (void __user *)arg); | |
248 | } | |
249 | ||
250 | return -ENOTTY; | |
251 | } | |
252 | ||
253 | /* Module initialization */ | |
254 | static const struct file_operations fops = { | |
255 | .open = ec_device_open, | |
256 | .release = ec_device_release, | |
257 | .read = ec_device_read, | |
258 | .unlocked_ioctl = ec_device_ioctl, | |
2521ea3e GR |
259 | #ifdef CONFIG_COMPAT |
260 | .compat_ioctl = ec_device_ioctl, | |
261 | #endif | |
e7c256fb BR |
262 | }; |
263 | ||
57b33ff0 GG |
264 | static void __remove(struct device *dev) |
265 | { | |
266 | struct cros_ec_dev *ec = container_of(dev, struct cros_ec_dev, | |
267 | class_dev); | |
268 | kfree(ec); | |
269 | } | |
270 | ||
5668bfdd EBS |
271 | static void cros_ec_sensors_register(struct cros_ec_dev *ec) |
272 | { | |
273 | /* | |
274 | * Issue a command to get the number of sensor reported. | |
275 | * Build an array of sensors driver and register them all. | |
276 | */ | |
277 | int ret, i, id, sensor_num; | |
278 | struct mfd_cell *sensor_cells; | |
279 | struct cros_ec_sensor_platform *sensor_platforms; | |
280 | int sensor_type[MOTIONSENSE_TYPE_MAX]; | |
281 | struct ec_params_motion_sense *params; | |
282 | struct ec_response_motion_sense *resp; | |
283 | struct cros_ec_command *msg; | |
284 | ||
285 | msg = kzalloc(sizeof(struct cros_ec_command) + | |
286 | max(sizeof(*params), sizeof(*resp)), GFP_KERNEL); | |
287 | if (msg == NULL) | |
288 | return; | |
289 | ||
290 | msg->version = 2; | |
291 | msg->command = EC_CMD_MOTION_SENSE_CMD + ec->cmd_offset; | |
292 | msg->outsize = sizeof(*params); | |
293 | msg->insize = sizeof(*resp); | |
294 | ||
295 | params = (struct ec_params_motion_sense *)msg->data; | |
296 | params->cmd = MOTIONSENSE_CMD_DUMP; | |
297 | ||
298 | ret = cros_ec_cmd_xfer(ec->ec_dev, msg); | |
299 | if (ret < 0 || msg->result != EC_RES_SUCCESS) { | |
300 | dev_warn(ec->dev, "cannot get EC sensor information: %d/%d\n", | |
301 | ret, msg->result); | |
302 | goto error; | |
303 | } | |
304 | ||
305 | resp = (struct ec_response_motion_sense *)msg->data; | |
306 | sensor_num = resp->dump.sensor_count; | |
307 | /* Allocate 2 extra sensors in case lid angle or FIFO are needed */ | |
308 | sensor_cells = kzalloc(sizeof(struct mfd_cell) * (sensor_num + 2), | |
309 | GFP_KERNEL); | |
310 | if (sensor_cells == NULL) | |
311 | goto error; | |
312 | ||
313 | sensor_platforms = kzalloc(sizeof(struct cros_ec_sensor_platform) * | |
314 | (sensor_num + 1), GFP_KERNEL); | |
315 | if (sensor_platforms == NULL) | |
316 | goto error_platforms; | |
317 | ||
318 | memset(sensor_type, 0, sizeof(sensor_type)); | |
319 | id = 0; | |
320 | for (i = 0; i < sensor_num; i++) { | |
321 | params->cmd = MOTIONSENSE_CMD_INFO; | |
322 | params->info.sensor_num = i; | |
323 | ret = cros_ec_cmd_xfer(ec->ec_dev, msg); | |
324 | if (ret < 0 || msg->result != EC_RES_SUCCESS) { | |
325 | dev_warn(ec->dev, "no info for EC sensor %d : %d/%d\n", | |
326 | i, ret, msg->result); | |
327 | continue; | |
328 | } | |
329 | switch (resp->info.type) { | |
330 | case MOTIONSENSE_TYPE_ACCEL: | |
331 | sensor_cells[id].name = "cros-ec-accel"; | |
332 | break; | |
d732248f GG |
333 | case MOTIONSENSE_TYPE_BARO: |
334 | sensor_cells[id].name = "cros-ec-baro"; | |
335 | break; | |
5668bfdd EBS |
336 | case MOTIONSENSE_TYPE_GYRO: |
337 | sensor_cells[id].name = "cros-ec-gyro"; | |
338 | break; | |
339 | case MOTIONSENSE_TYPE_MAG: | |
340 | sensor_cells[id].name = "cros-ec-mag"; | |
341 | break; | |
342 | case MOTIONSENSE_TYPE_PROX: | |
343 | sensor_cells[id].name = "cros-ec-prox"; | |
344 | break; | |
345 | case MOTIONSENSE_TYPE_LIGHT: | |
346 | sensor_cells[id].name = "cros-ec-light"; | |
347 | break; | |
348 | case MOTIONSENSE_TYPE_ACTIVITY: | |
349 | sensor_cells[id].name = "cros-ec-activity"; | |
350 | break; | |
351 | default: | |
352 | dev_warn(ec->dev, "unknown type %d\n", resp->info.type); | |
353 | continue; | |
354 | } | |
355 | sensor_platforms[id].sensor_num = i; | |
356 | sensor_cells[id].id = sensor_type[resp->info.type]; | |
357 | sensor_cells[id].platform_data = &sensor_platforms[id]; | |
358 | sensor_cells[id].pdata_size = | |
359 | sizeof(struct cros_ec_sensor_platform); | |
360 | ||
361 | sensor_type[resp->info.type]++; | |
362 | id++; | |
363 | } | |
364 | if (sensor_type[MOTIONSENSE_TYPE_ACCEL] >= 2) { | |
365 | sensor_platforms[id].sensor_num = sensor_num; | |
366 | ||
367 | sensor_cells[id].name = "cros-ec-angle"; | |
368 | sensor_cells[id].id = 0; | |
369 | sensor_cells[id].platform_data = &sensor_platforms[id]; | |
370 | sensor_cells[id].pdata_size = | |
371 | sizeof(struct cros_ec_sensor_platform); | |
372 | id++; | |
373 | } | |
374 | if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE_FIFO)) { | |
375 | sensor_cells[id].name = "cros-ec-ring"; | |
376 | id++; | |
377 | } | |
378 | ||
379 | ret = mfd_add_devices(ec->dev, 0, sensor_cells, id, | |
380 | NULL, 0, NULL); | |
381 | if (ret) | |
382 | dev_err(ec->dev, "failed to add EC sensors\n"); | |
383 | ||
384 | kfree(sensor_platforms); | |
385 | error_platforms: | |
386 | kfree(sensor_cells); | |
387 | error: | |
388 | kfree(msg); | |
389 | } | |
390 | ||
e7c256fb BR |
391 | static int ec_device_probe(struct platform_device *pdev) |
392 | { | |
57b33ff0 GG |
393 | int retval = -ENOMEM; |
394 | struct device *dev = &pdev->dev; | |
395 | struct cros_ec_platform *ec_platform = dev_get_platdata(dev); | |
57b33ff0 GG |
396 | struct cros_ec_dev *ec = kzalloc(sizeof(*ec), GFP_KERNEL); |
397 | ||
398 | if (!ec) | |
399 | return retval; | |
e7c256fb | 400 | |
57b33ff0 GG |
401 | dev_set_drvdata(dev, ec); |
402 | ec->ec_dev = dev_get_drvdata(dev->parent); | |
403 | ec->dev = dev; | |
404 | ec->cmd_offset = ec_platform->cmd_offset; | |
e4244ebd VP |
405 | ec->features[0] = -1U; /* Not cached yet */ |
406 | ec->features[1] = -1U; /* Not cached yet */ | |
57b33ff0 | 407 | device_initialize(&ec->class_dev); |
e7c256fb BR |
408 | cdev_init(&ec->cdev, &fops); |
409 | ||
57b33ff0 GG |
410 | /* |
411 | * Add the class device | |
412 | * Link to the character device for creating the /dev entry | |
413 | * in devtmpfs. | |
414 | */ | |
1c1d152c | 415 | ec->class_dev.devt = MKDEV(ec_major, pdev->id); |
57b33ff0 GG |
416 | ec->class_dev.class = &cros_class; |
417 | ec->class_dev.parent = dev; | |
418 | ec->class_dev.release = __remove; | |
419 | ||
420 | retval = dev_set_name(&ec->class_dev, "%s", ec_platform->ec_name); | |
421 | if (retval) { | |
422 | dev_err(dev, "dev_set_name failed => %d\n", retval); | |
1c1d152c | 423 | goto failed; |
e7c256fb BR |
424 | } |
425 | ||
1c1d152c | 426 | retval = cdev_device_add(&ec->cdev, &ec->class_dev); |
57b33ff0 | 427 | if (retval) { |
1c1d152c LG |
428 | dev_err(dev, "cdev_device_add failed => %d\n", retval); |
429 | goto failed; | |
57b33ff0 | 430 | } |
71af4b52 | 431 | |
e8626459 EC |
432 | if (cros_ec_debugfs_init(ec)) |
433 | dev_warn(dev, "failed to create debugfs directory\n"); | |
434 | ||
5668bfdd EBS |
435 | /* check whether this EC is a sensor hub. */ |
436 | if (cros_ec_check_features(ec, EC_FEATURE_MOTION_SENSE)) | |
437 | cros_ec_sensors_register(ec); | |
438 | ||
405c8430 | 439 | /* Take control of the lightbar from the EC. */ |
995c0ec9 | 440 | lb_manual_suspend_ctrl(ec, 1); |
405c8430 | 441 | |
e7c256fb | 442 | return 0; |
57b33ff0 | 443 | |
1c1d152c LG |
444 | failed: |
445 | put_device(&ec->class_dev); | |
57b33ff0 | 446 | return retval; |
e7c256fb BR |
447 | } |
448 | ||
449 | static int ec_device_remove(struct platform_device *pdev) | |
450 | { | |
57b33ff0 | 451 | struct cros_ec_dev *ec = dev_get_drvdata(&pdev->dev); |
e8626459 | 452 | |
405c8430 | 453 | /* Let the EC take over the lightbar again. */ |
995c0ec9 | 454 | lb_manual_suspend_ctrl(ec, 0); |
405c8430 | 455 | |
e8626459 EC |
456 | cros_ec_debugfs_remove(ec); |
457 | ||
e7c256fb | 458 | cdev_del(&ec->cdev); |
57b33ff0 | 459 | device_unregister(&ec->class_dev); |
e7c256fb BR |
460 | return 0; |
461 | } | |
462 | ||
afbf8ec7 JMC |
463 | static const struct platform_device_id cros_ec_id[] = { |
464 | { "cros-ec-ctl", 0 }, | |
465 | { /* sentinel */ }, | |
466 | }; | |
467 | MODULE_DEVICE_TABLE(platform, cros_ec_id); | |
468 | ||
5d6a312e | 469 | static __maybe_unused int ec_device_suspend(struct device *dev) |
405c8430 EC |
470 | { |
471 | struct cros_ec_dev *ec = dev_get_drvdata(dev); | |
472 | ||
995c0ec9 | 473 | lb_suspend(ec); |
405c8430 EC |
474 | |
475 | return 0; | |
476 | } | |
477 | ||
5d6a312e | 478 | static __maybe_unused int ec_device_resume(struct device *dev) |
405c8430 EC |
479 | { |
480 | struct cros_ec_dev *ec = dev_get_drvdata(dev); | |
481 | ||
995c0ec9 | 482 | lb_resume(ec); |
405c8430 EC |
483 | |
484 | return 0; | |
485 | } | |
486 | ||
487 | static const struct dev_pm_ops cros_ec_dev_pm_ops = { | |
488 | #ifdef CONFIG_PM_SLEEP | |
489 | .suspend = ec_device_suspend, | |
490 | .resume = ec_device_resume, | |
491 | #endif | |
492 | }; | |
493 | ||
e7c256fb BR |
494 | static struct platform_driver cros_ec_dev_driver = { |
495 | .driver = { | |
496 | .name = "cros-ec-ctl", | |
405c8430 | 497 | .pm = &cros_ec_dev_pm_ops, |
e7c256fb BR |
498 | }, |
499 | .probe = ec_device_probe, | |
500 | .remove = ec_device_remove, | |
501 | }; | |
502 | ||
503 | static int __init cros_ec_dev_init(void) | |
504 | { | |
505 | int ret; | |
506 | dev_t dev = 0; | |
507 | ||
57b33ff0 GG |
508 | ret = class_register(&cros_class); |
509 | if (ret) { | |
e7c256fb | 510 | pr_err(CROS_EC_DEV_NAME ": failed to register device class\n"); |
57b33ff0 | 511 | return ret; |
e7c256fb BR |
512 | } |
513 | ||
514 | /* Get a range of minor numbers (starting with 0) to work with */ | |
515 | ret = alloc_chrdev_region(&dev, 0, CROS_MAX_DEV, CROS_EC_DEV_NAME); | |
516 | if (ret < 0) { | |
517 | pr_err(CROS_EC_DEV_NAME ": alloc_chrdev_region() failed\n"); | |
518 | goto failed_chrdevreg; | |
519 | } | |
520 | ec_major = MAJOR(dev); | |
521 | ||
522 | /* Register the driver */ | |
523 | ret = platform_driver_register(&cros_ec_dev_driver); | |
524 | if (ret < 0) { | |
525 | pr_warn(CROS_EC_DEV_NAME ": can't register driver: %d\n", ret); | |
526 | goto failed_devreg; | |
527 | } | |
528 | return 0; | |
529 | ||
530 | failed_devreg: | |
531 | unregister_chrdev_region(MKDEV(ec_major, 0), CROS_MAX_DEV); | |
532 | failed_chrdevreg: | |
57b33ff0 | 533 | class_unregister(&cros_class); |
e7c256fb BR |
534 | return ret; |
535 | } | |
536 | ||
537 | static void __exit cros_ec_dev_exit(void) | |
538 | { | |
539 | platform_driver_unregister(&cros_ec_dev_driver); | |
540 | unregister_chrdev(ec_major, CROS_EC_DEV_NAME); | |
57b33ff0 | 541 | class_unregister(&cros_class); |
e7c256fb BR |
542 | } |
543 | ||
544 | module_init(cros_ec_dev_init); | |
545 | module_exit(cros_ec_dev_exit); | |
546 | ||
547 | MODULE_AUTHOR("Bill Richardson <wfrichar@chromium.org>"); | |
548 | MODULE_DESCRIPTION("Userspace interface to the Chrome OS Embedded Controller"); | |
549 | MODULE_VERSION("1.0"); | |
550 | MODULE_LICENSE("GPL"); |