Commit | Line | Data |
---|---|---|
136200f4 | 1 | // SPDX-License-Identifier: GPL-2.0 |
c0cdc19f | 2 | /* |
69265bc1 | 3 | * Copyright (C) 2022, STMicroelectronics |
c0cdc19f BA |
4 | * Copyright (c) 2016, Linaro Ltd. |
5 | * Copyright (c) 2012, Michal Simek <monstr@monstr.eu> | |
6 | * Copyright (c) 2012, PetaLogix | |
7 | * Copyright (c) 2011, Texas Instruments, Inc. | |
8 | * Copyright (c) 2011, Google, Inc. | |
9 | * | |
10 | * Based on rpmsg performance statistics driver by Michal Simek, which in turn | |
11 | * was based on TI & Google OMX rpmsg driver. | |
c0cdc19f | 12 | */ |
c5727244 AP |
13 | |
14 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
15 | ||
c0cdc19f BA |
16 | #include <linux/cdev.h> |
17 | #include <linux/device.h> | |
18 | #include <linux/fs.h> | |
19 | #include <linux/idr.h> | |
20 | #include <linux/kernel.h> | |
21 | #include <linux/module.h> | |
22 | #include <linux/poll.h> | |
23 | #include <linux/rpmsg.h> | |
24 | #include <linux/skbuff.h> | |
25 | #include <linux/slab.h> | |
26 | #include <linux/uaccess.h> | |
27 | #include <uapi/linux/rpmsg.h> | |
28 | ||
69265bc1 | 29 | #include "rpmsg_char.h" |
608edd96 | 30 | #include "rpmsg_internal.h" |
69265bc1 | 31 | |
c0cdc19f BA |
32 | #define RPMSG_DEV_MAX (MINORMASK + 1) |
33 | ||
34 | static dev_t rpmsg_major; | |
c0cdc19f | 35 | |
c0cdc19f BA |
36 | static DEFINE_IDA(rpmsg_ept_ida); |
37 | static DEFINE_IDA(rpmsg_minor_ida); | |
38 | ||
39 | #define dev_to_eptdev(dev) container_of(dev, struct rpmsg_eptdev, dev) | |
40 | #define cdev_to_eptdev(i_cdev) container_of(i_cdev, struct rpmsg_eptdev, cdev) | |
41 | ||
c0cdc19f BA |
42 | /** |
43 | * struct rpmsg_eptdev - endpoint device context | |
44 | * @dev: endpoint device | |
45 | * @cdev: cdev for the endpoint device | |
46 | * @rpdev: underlaying rpmsg device | |
47 | * @chinfo: info used to open the endpoint | |
48 | * @ept_lock: synchronization of @ept modifications | |
49 | * @ept: rpmsg endpoint reference, when open | |
50 | * @queue_lock: synchronization of @queue operations | |
51 | * @queue: incoming message queue | |
52 | * @readq: wait object for incoming queue | |
bea9b79c AP |
53 | * @default_ept: set to channel default endpoint if the default endpoint should be re-used |
54 | * on device open to prevent endpoint address update. | |
c0cdc19f BA |
55 | */ |
56 | struct rpmsg_eptdev { | |
57 | struct device dev; | |
58 | struct cdev cdev; | |
59 | ||
60 | struct rpmsg_device *rpdev; | |
61 | struct rpmsg_channel_info chinfo; | |
62 | ||
63 | struct mutex ept_lock; | |
64 | struct rpmsg_endpoint *ept; | |
bea9b79c | 65 | struct rpmsg_endpoint *default_ept; |
c0cdc19f BA |
66 | |
67 | spinlock_t queue_lock; | |
68 | struct sk_buff_head queue; | |
69 | wait_queue_head_t readq; | |
bea9b79c | 70 | |
c0cdc19f BA |
71 | }; |
72 | ||
69265bc1 | 73 | int rpmsg_chrdev_eptdev_destroy(struct device *dev, void *data) |
c0cdc19f BA |
74 | { |
75 | struct rpmsg_eptdev *eptdev = dev_to_eptdev(dev); | |
76 | ||
77 | mutex_lock(&eptdev->ept_lock); | |
17b88a20 | 78 | eptdev->rpdev = NULL; |
c0cdc19f | 79 | if (eptdev->ept) { |
467233a4 SW |
80 | /* The default endpoint is released by the rpmsg core */ |
81 | if (!eptdev->default_ept) | |
82 | rpmsg_destroy_ept(eptdev->ept); | |
c0cdc19f BA |
83 | eptdev->ept = NULL; |
84 | } | |
85 | mutex_unlock(&eptdev->ept_lock); | |
86 | ||
87 | /* wake up any blocked readers */ | |
88 | wake_up_interruptible(&eptdev->readq); | |
89 | ||
7a534ae8 | 90 | cdev_device_del(&eptdev->cdev, &eptdev->dev); |
c0cdc19f BA |
91 | put_device(&eptdev->dev); |
92 | ||
93 | return 0; | |
94 | } | |
69265bc1 | 95 | EXPORT_SYMBOL(rpmsg_chrdev_eptdev_destroy); |
c0cdc19f BA |
96 | |
97 | static int rpmsg_ept_cb(struct rpmsg_device *rpdev, void *buf, int len, | |
98 | void *priv, u32 addr) | |
99 | { | |
100 | struct rpmsg_eptdev *eptdev = priv; | |
101 | struct sk_buff *skb; | |
102 | ||
103 | skb = alloc_skb(len, GFP_ATOMIC); | |
104 | if (!skb) | |
105 | return -ENOMEM; | |
106 | ||
59ae1d12 | 107 | skb_put_data(skb, buf, len); |
c0cdc19f BA |
108 | |
109 | spin_lock(&eptdev->queue_lock); | |
110 | skb_queue_tail(&eptdev->queue, skb); | |
111 | spin_unlock(&eptdev->queue_lock); | |
112 | ||
113 | /* wake up any blocking processes, waiting for new data */ | |
114 | wake_up_interruptible(&eptdev->readq); | |
115 | ||
116 | return 0; | |
117 | } | |
118 | ||
119 | static int rpmsg_eptdev_open(struct inode *inode, struct file *filp) | |
120 | { | |
121 | struct rpmsg_eptdev *eptdev = cdev_to_eptdev(inode->i_cdev); | |
122 | struct rpmsg_endpoint *ept; | |
123 | struct rpmsg_device *rpdev = eptdev->rpdev; | |
124 | struct device *dev = &eptdev->dev; | |
125 | ||
abe13e9a SW |
126 | mutex_lock(&eptdev->ept_lock); |
127 | if (eptdev->ept) { | |
128 | mutex_unlock(&eptdev->ept_lock); | |
964e8bed | 129 | return -EBUSY; |
abe13e9a | 130 | } |
964e8bed | 131 | |
17b88a20 DKS |
132 | if (!eptdev->rpdev) { |
133 | mutex_unlock(&eptdev->ept_lock); | |
134 | return -ENETRESET; | |
135 | } | |
136 | ||
c0cdc19f BA |
137 | get_device(dev); |
138 | ||
bea9b79c AP |
139 | /* |
140 | * If the default_ept is set, the rpmsg device default endpoint is used. | |
141 | * Else a new endpoint is created on open that will be destroyed on release. | |
142 | */ | |
143 | if (eptdev->default_ept) | |
144 | ept = eptdev->default_ept; | |
145 | else | |
146 | ept = rpmsg_create_ept(rpdev, rpmsg_ept_cb, eptdev, eptdev->chinfo); | |
147 | ||
c0cdc19f BA |
148 | if (!ept) { |
149 | dev_err(dev, "failed to open %s\n", eptdev->chinfo.name); | |
150 | put_device(dev); | |
abe13e9a | 151 | mutex_unlock(&eptdev->ept_lock); |
c0cdc19f BA |
152 | return -EINVAL; |
153 | } | |
154 | ||
155 | eptdev->ept = ept; | |
156 | filp->private_data = eptdev; | |
abe13e9a | 157 | mutex_unlock(&eptdev->ept_lock); |
c0cdc19f BA |
158 | |
159 | return 0; | |
160 | } | |
161 | ||
162 | static int rpmsg_eptdev_release(struct inode *inode, struct file *filp) | |
163 | { | |
164 | struct rpmsg_eptdev *eptdev = cdev_to_eptdev(inode->i_cdev); | |
165 | struct device *dev = &eptdev->dev; | |
c0cdc19f BA |
166 | |
167 | /* Close the endpoint, if it's not already destroyed by the parent */ | |
168 | mutex_lock(&eptdev->ept_lock); | |
169 | if (eptdev->ept) { | |
bea9b79c AP |
170 | if (!eptdev->default_ept) |
171 | rpmsg_destroy_ept(eptdev->ept); | |
c0cdc19f BA |
172 | eptdev->ept = NULL; |
173 | } | |
174 | mutex_unlock(&eptdev->ept_lock); | |
175 | ||
176 | /* Discard all SKBs */ | |
bb06a5ce | 177 | skb_queue_purge(&eptdev->queue); |
c0cdc19f BA |
178 | |
179 | put_device(dev); | |
180 | ||
181 | return 0; | |
182 | } | |
183 | ||
ccf45b18 | 184 | static ssize_t rpmsg_eptdev_read_iter(struct kiocb *iocb, struct iov_iter *to) |
c0cdc19f | 185 | { |
ccf45b18 | 186 | struct file *filp = iocb->ki_filp; |
c0cdc19f BA |
187 | struct rpmsg_eptdev *eptdev = filp->private_data; |
188 | unsigned long flags; | |
189 | struct sk_buff *skb; | |
190 | int use; | |
191 | ||
192 | if (!eptdev->ept) | |
193 | return -EPIPE; | |
194 | ||
195 | spin_lock_irqsave(&eptdev->queue_lock, flags); | |
196 | ||
197 | /* Wait for data in the queue */ | |
198 | if (skb_queue_empty(&eptdev->queue)) { | |
199 | spin_unlock_irqrestore(&eptdev->queue_lock, flags); | |
200 | ||
201 | if (filp->f_flags & O_NONBLOCK) | |
202 | return -EAGAIN; | |
203 | ||
204 | /* Wait until we get data or the endpoint goes away */ | |
205 | if (wait_event_interruptible(eptdev->readq, | |
206 | !skb_queue_empty(&eptdev->queue) || | |
207 | !eptdev->ept)) | |
208 | return -ERESTARTSYS; | |
209 | ||
210 | /* We lost the endpoint while waiting */ | |
211 | if (!eptdev->ept) | |
212 | return -EPIPE; | |
213 | ||
214 | spin_lock_irqsave(&eptdev->queue_lock, flags); | |
215 | } | |
216 | ||
217 | skb = skb_dequeue(&eptdev->queue); | |
0abd6bdd | 218 | spin_unlock_irqrestore(&eptdev->queue_lock, flags); |
c0cdc19f BA |
219 | if (!skb) |
220 | return -EFAULT; | |
221 | ||
ccf45b18 BA |
222 | use = min_t(size_t, iov_iter_count(to), skb->len); |
223 | if (copy_to_iter(skb->data, use, to) != use) | |
c0cdc19f BA |
224 | use = -EFAULT; |
225 | ||
226 | kfree_skb(skb); | |
227 | ||
228 | return use; | |
229 | } | |
230 | ||
ccf45b18 BA |
231 | static ssize_t rpmsg_eptdev_write_iter(struct kiocb *iocb, |
232 | struct iov_iter *from) | |
c0cdc19f | 233 | { |
ccf45b18 | 234 | struct file *filp = iocb->ki_filp; |
c0cdc19f | 235 | struct rpmsg_eptdev *eptdev = filp->private_data; |
ccf45b18 | 236 | size_t len = iov_iter_count(from); |
c0cdc19f BA |
237 | void *kbuf; |
238 | int ret; | |
239 | ||
ccf45b18 BA |
240 | kbuf = kzalloc(len, GFP_KERNEL); |
241 | if (!kbuf) | |
242 | return -ENOMEM; | |
243 | ||
bbe692e3 NE |
244 | if (!copy_from_iter_full(kbuf, len, from)) { |
245 | ret = -EFAULT; | |
246 | goto free_kbuf; | |
247 | } | |
c0cdc19f BA |
248 | |
249 | if (mutex_lock_interruptible(&eptdev->ept_lock)) { | |
250 | ret = -ERESTARTSYS; | |
251 | goto free_kbuf; | |
252 | } | |
253 | ||
254 | if (!eptdev->ept) { | |
255 | ret = -EPIPE; | |
256 | goto unlock_eptdev; | |
257 | } | |
258 | ||
cbf58250 | 259 | if (filp->f_flags & O_NONBLOCK) { |
b4ce7e2e | 260 | ret = rpmsg_trysendto(eptdev->ept, kbuf, len, eptdev->chinfo.dst); |
cbf58250 TB |
261 | if (ret == -ENOMEM) |
262 | ret = -EAGAIN; | |
263 | } else { | |
b4ce7e2e | 264 | ret = rpmsg_sendto(eptdev->ept, kbuf, len, eptdev->chinfo.dst); |
cbf58250 | 265 | } |
c0cdc19f BA |
266 | |
267 | unlock_eptdev: | |
268 | mutex_unlock(&eptdev->ept_lock); | |
269 | ||
270 | free_kbuf: | |
271 | kfree(kbuf); | |
272 | return ret < 0 ? ret : len; | |
273 | } | |
274 | ||
afc9a42b | 275 | static __poll_t rpmsg_eptdev_poll(struct file *filp, poll_table *wait) |
c0cdc19f BA |
276 | { |
277 | struct rpmsg_eptdev *eptdev = filp->private_data; | |
afc9a42b | 278 | __poll_t mask = 0; |
c0cdc19f BA |
279 | |
280 | if (!eptdev->ept) | |
a9a08845 | 281 | return EPOLLERR; |
c0cdc19f BA |
282 | |
283 | poll_wait(filp, &eptdev->readq, wait); | |
284 | ||
285 | if (!skb_queue_empty(&eptdev->queue)) | |
a9a08845 | 286 | mask |= EPOLLIN | EPOLLRDNORM; |
c0cdc19f | 287 | |
17b88a20 | 288 | mutex_lock(&eptdev->ept_lock); |
c0cdc19f | 289 | mask |= rpmsg_poll(eptdev->ept, filp, wait); |
17b88a20 | 290 | mutex_unlock(&eptdev->ept_lock); |
c0cdc19f BA |
291 | |
292 | return mask; | |
293 | } | |
294 | ||
295 | static long rpmsg_eptdev_ioctl(struct file *fp, unsigned int cmd, | |
296 | unsigned long arg) | |
297 | { | |
298 | struct rpmsg_eptdev *eptdev = fp->private_data; | |
299 | ||
300 | if (cmd != RPMSG_DESTROY_EPT_IOCTL) | |
301 | return -EINVAL; | |
302 | ||
bea9b79c AP |
303 | /* Don't allow to destroy a default endpoint. */ |
304 | if (eptdev->default_ept) | |
305 | return -EINVAL; | |
306 | ||
69265bc1 | 307 | return rpmsg_chrdev_eptdev_destroy(&eptdev->dev, NULL); |
c0cdc19f BA |
308 | } |
309 | ||
310 | static const struct file_operations rpmsg_eptdev_fops = { | |
311 | .owner = THIS_MODULE, | |
312 | .open = rpmsg_eptdev_open, | |
313 | .release = rpmsg_eptdev_release, | |
ccf45b18 BA |
314 | .read_iter = rpmsg_eptdev_read_iter, |
315 | .write_iter = rpmsg_eptdev_write_iter, | |
c0cdc19f BA |
316 | .poll = rpmsg_eptdev_poll, |
317 | .unlocked_ioctl = rpmsg_eptdev_ioctl, | |
1832f2d8 | 318 | .compat_ioctl = compat_ptr_ioctl, |
c0cdc19f BA |
319 | }; |
320 | ||
321 | static ssize_t name_show(struct device *dev, struct device_attribute *attr, | |
322 | char *buf) | |
323 | { | |
324 | struct rpmsg_eptdev *eptdev = dev_get_drvdata(dev); | |
325 | ||
326 | return sprintf(buf, "%s\n", eptdev->chinfo.name); | |
327 | } | |
328 | static DEVICE_ATTR_RO(name); | |
329 | ||
330 | static ssize_t src_show(struct device *dev, struct device_attribute *attr, | |
331 | char *buf) | |
332 | { | |
333 | struct rpmsg_eptdev *eptdev = dev_get_drvdata(dev); | |
334 | ||
335 | return sprintf(buf, "%d\n", eptdev->chinfo.src); | |
336 | } | |
337 | static DEVICE_ATTR_RO(src); | |
338 | ||
339 | static ssize_t dst_show(struct device *dev, struct device_attribute *attr, | |
340 | char *buf) | |
341 | { | |
342 | struct rpmsg_eptdev *eptdev = dev_get_drvdata(dev); | |
343 | ||
344 | return sprintf(buf, "%d\n", eptdev->chinfo.dst); | |
345 | } | |
346 | static DEVICE_ATTR_RO(dst); | |
347 | ||
348 | static struct attribute *rpmsg_eptdev_attrs[] = { | |
349 | &dev_attr_name.attr, | |
350 | &dev_attr_src.attr, | |
351 | &dev_attr_dst.attr, | |
352 | NULL | |
353 | }; | |
354 | ATTRIBUTE_GROUPS(rpmsg_eptdev); | |
355 | ||
356 | static void rpmsg_eptdev_release_device(struct device *dev) | |
357 | { | |
358 | struct rpmsg_eptdev *eptdev = dev_to_eptdev(dev); | |
359 | ||
360 | ida_simple_remove(&rpmsg_ept_ida, dev->id); | |
361 | ida_simple_remove(&rpmsg_minor_ida, MINOR(eptdev->dev.devt)); | |
c0cdc19f BA |
362 | kfree(eptdev); |
363 | } | |
364 | ||
cc9da7de AP |
365 | static struct rpmsg_eptdev *rpmsg_chrdev_eptdev_alloc(struct rpmsg_device *rpdev, |
366 | struct device *parent) | |
c0cdc19f | 367 | { |
c0cdc19f BA |
368 | struct rpmsg_eptdev *eptdev; |
369 | struct device *dev; | |
c0cdc19f BA |
370 | |
371 | eptdev = kzalloc(sizeof(*eptdev), GFP_KERNEL); | |
372 | if (!eptdev) | |
cc9da7de | 373 | return ERR_PTR(-ENOMEM); |
c0cdc19f BA |
374 | |
375 | dev = &eptdev->dev; | |
376 | eptdev->rpdev = rpdev; | |
c0cdc19f BA |
377 | |
378 | mutex_init(&eptdev->ept_lock); | |
379 | spin_lock_init(&eptdev->queue_lock); | |
380 | skb_queue_head_init(&eptdev->queue); | |
381 | init_waitqueue_head(&eptdev->readq); | |
382 | ||
383 | device_initialize(dev); | |
384 | dev->class = rpmsg_class; | |
69265bc1 | 385 | dev->parent = parent; |
c0cdc19f BA |
386 | dev->groups = rpmsg_eptdev_groups; |
387 | dev_set_drvdata(dev, eptdev); | |
388 | ||
389 | cdev_init(&eptdev->cdev, &rpmsg_eptdev_fops); | |
390 | eptdev->cdev.owner = THIS_MODULE; | |
391 | ||
cc9da7de AP |
392 | return eptdev; |
393 | } | |
394 | ||
395 | static int rpmsg_chrdev_eptdev_add(struct rpmsg_eptdev *eptdev, struct rpmsg_channel_info chinfo) | |
396 | { | |
397 | struct device *dev = &eptdev->dev; | |
398 | int ret; | |
399 | ||
400 | eptdev->chinfo = chinfo; | |
401 | ||
c0cdc19f BA |
402 | ret = ida_simple_get(&rpmsg_minor_ida, 0, RPMSG_DEV_MAX, GFP_KERNEL); |
403 | if (ret < 0) | |
404 | goto free_eptdev; | |
405 | dev->devt = MKDEV(MAJOR(rpmsg_major), ret); | |
406 | ||
407 | ret = ida_simple_get(&rpmsg_ept_ida, 0, 0, GFP_KERNEL); | |
408 | if (ret < 0) | |
409 | goto free_minor_ida; | |
410 | dev->id = ret; | |
411 | dev_set_name(dev, "rpmsg%d", ret); | |
412 | ||
7a534ae8 | 413 | ret = cdev_device_add(&eptdev->cdev, &eptdev->dev); |
c0cdc19f BA |
414 | if (ret) |
415 | goto free_ept_ida; | |
416 | ||
417 | /* We can now rely on the release function for cleanup */ | |
418 | dev->release = rpmsg_eptdev_release_device; | |
419 | ||
c0cdc19f BA |
420 | return ret; |
421 | ||
422 | free_ept_ida: | |
423 | ida_simple_remove(&rpmsg_ept_ida, dev->id); | |
424 | free_minor_ida: | |
425 | ida_simple_remove(&rpmsg_minor_ida, MINOR(dev->devt)); | |
426 | free_eptdev: | |
427 | put_device(dev); | |
428 | kfree(eptdev); | |
429 | ||
430 | return ret; | |
431 | } | |
cc9da7de AP |
432 | |
433 | int rpmsg_chrdev_eptdev_create(struct rpmsg_device *rpdev, struct device *parent, | |
434 | struct rpmsg_channel_info chinfo) | |
435 | { | |
436 | struct rpmsg_eptdev *eptdev; | |
cc9da7de AP |
437 | |
438 | eptdev = rpmsg_chrdev_eptdev_alloc(rpdev, parent); | |
439 | if (IS_ERR(eptdev)) | |
440 | return PTR_ERR(eptdev); | |
441 | ||
06564be4 | 442 | return rpmsg_chrdev_eptdev_add(eptdev, chinfo); |
cc9da7de | 443 | } |
69265bc1 | 444 | EXPORT_SYMBOL(rpmsg_chrdev_eptdev_create); |
c0cdc19f | 445 | |
bc69d106 AP |
446 | static int rpmsg_chrdev_probe(struct rpmsg_device *rpdev) |
447 | { | |
448 | struct rpmsg_channel_info chinfo; | |
449 | struct rpmsg_eptdev *eptdev; | |
450 | struct device *dev = &rpdev->dev; | |
451 | ||
452 | memcpy(chinfo.name, rpdev->id.name, RPMSG_NAME_SIZE); | |
453 | chinfo.src = rpdev->src; | |
454 | chinfo.dst = rpdev->dst; | |
455 | ||
456 | eptdev = rpmsg_chrdev_eptdev_alloc(rpdev, dev); | |
457 | if (IS_ERR(eptdev)) | |
458 | return PTR_ERR(eptdev); | |
459 | ||
460 | /* Set the default_ept to the rpmsg device endpoint */ | |
461 | eptdev->default_ept = rpdev->ept; | |
462 | ||
463 | /* | |
464 | * The rpmsg_ept_cb uses *priv parameter to get its rpmsg_eptdev context. | |
465 | * Storedit in default_ept *priv field. | |
466 | */ | |
467 | eptdev->default_ept->priv = eptdev; | |
468 | ||
469 | return rpmsg_chrdev_eptdev_add(eptdev, chinfo); | |
470 | } | |
471 | ||
472 | static void rpmsg_chrdev_remove(struct rpmsg_device *rpdev) | |
473 | { | |
474 | int ret; | |
475 | ||
476 | ret = device_for_each_child(&rpdev->dev, NULL, rpmsg_chrdev_eptdev_destroy); | |
477 | if (ret) | |
478 | dev_warn(&rpdev->dev, "failed to destroy endpoints: %d\n", ret); | |
479 | } | |
480 | ||
481 | static struct rpmsg_device_id rpmsg_chrdev_id_table[] = { | |
482 | { .name = "rpmsg-raw" }, | |
483 | { }, | |
484 | }; | |
485 | ||
486 | static struct rpmsg_driver rpmsg_chrdev_driver = { | |
487 | .probe = rpmsg_chrdev_probe, | |
488 | .remove = rpmsg_chrdev_remove, | |
489 | .callback = rpmsg_ept_cb, | |
490 | .id_table = rpmsg_chrdev_id_table, | |
491 | .drv.name = "rpmsg_chrdev", | |
492 | }; | |
493 | ||
60d7b22d | 494 | static int rpmsg_chrdev_init(void) |
c0cdc19f BA |
495 | { |
496 | int ret; | |
497 | ||
617d3293 | 498 | ret = alloc_chrdev_region(&rpmsg_major, 0, RPMSG_DEV_MAX, "rpmsg_char"); |
c0cdc19f | 499 | if (ret < 0) { |
c5727244 | 500 | pr_err("failed to allocate char dev region\n"); |
c0cdc19f BA |
501 | return ret; |
502 | } | |
503 | ||
bc69d106 AP |
504 | ret = register_rpmsg_driver(&rpmsg_chrdev_driver); |
505 | if (ret < 0) { | |
506 | pr_err("rpmsg: failed to register rpmsg raw driver\n"); | |
507 | goto free_region; | |
508 | } | |
509 | ||
617d3293 | 510 | return 0; |
bc69d106 AP |
511 | |
512 | free_region: | |
513 | unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX); | |
514 | ||
515 | return ret; | |
c0cdc19f | 516 | } |
60d7b22d | 517 | postcore_initcall(rpmsg_chrdev_init); |
c0cdc19f BA |
518 | |
519 | static void rpmsg_chrdev_exit(void) | |
520 | { | |
bc69d106 | 521 | unregister_rpmsg_driver(&rpmsg_chrdev_driver); |
c0cdc19f BA |
522 | unregister_chrdev_region(rpmsg_major, RPMSG_DEV_MAX); |
523 | } | |
524 | module_exit(rpmsg_chrdev_exit); | |
93dd4e73 RF |
525 | |
526 | MODULE_ALIAS("rpmsg:rpmsg_chrdev"); | |
c0cdc19f | 527 | MODULE_LICENSE("GPL v2"); |