1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright 2021 Xillybus Ltd, http://xillybus.com |
4 | * |
5 | * Driver for the Xillybus class |
6 | */ |
7 | |
8 | #include <linux/types.h> |
9 | #include <linux/module.h> |
10 | #include <linux/device.h> |
11 | #include <linux/fs.h> |
12 | #include <linux/cdev.h> |
13 | #include <linux/slab.h> |
14 | #include <linux/list.h> |
15 | #include <linux/mutex.h> |
16 | |
17 | #include "xillybus_class.h" |
18 | |
19 | MODULE_DESCRIPTION("Driver for Xillybus class" ); |
20 | MODULE_AUTHOR("Eli Billauer, Xillybus Ltd." ); |
21 | MODULE_ALIAS("xillybus_class" ); |
22 | MODULE_LICENSE("GPL v2" ); |
23 | |
24 | static DEFINE_MUTEX(unit_mutex); |
25 | static LIST_HEAD(unit_list); |
26 | static const struct class xillybus_class = { |
27 | .name = "xillybus" , |
28 | }; |
29 | |
30 | #define UNITNAMELEN 16 |
31 | |
32 | struct xilly_unit { |
33 | struct list_head list_entry; |
34 | void *private_data; |
35 | |
36 | struct cdev *cdev; |
37 | char name[UNITNAMELEN]; |
38 | int major; |
39 | int lowest_minor; |
40 | int num_nodes; |
41 | }; |
42 | |
43 | int xillybus_init_chrdev(struct device *dev, |
44 | const struct file_operations *fops, |
45 | struct module *owner, |
46 | void *private_data, |
47 | unsigned char *idt, unsigned int len, |
48 | int num_nodes, |
49 | const char *prefix, bool enumerate) |
50 | { |
51 | int rc; |
52 | dev_t mdev; |
53 | int i; |
54 | char devname[48]; |
55 | |
56 | struct device *device; |
57 | size_t namelen; |
58 | struct xilly_unit *unit, *u; |
59 | |
60 | unit = kzalloc(size: sizeof(*unit), GFP_KERNEL); |
61 | |
62 | if (!unit) |
63 | return -ENOMEM; |
64 | |
65 | mutex_lock(&unit_mutex); |
66 | |
67 | if (!enumerate) |
68 | snprintf(buf: unit->name, UNITNAMELEN, fmt: "%s" , prefix); |
69 | |
70 | for (i = 0; enumerate; i++) { |
71 | snprintf(buf: unit->name, UNITNAMELEN, fmt: "%s_%02d" , |
72 | prefix, i); |
73 | |
74 | enumerate = false; |
75 | list_for_each_entry(u, &unit_list, list_entry) |
76 | if (!strcmp(unit->name, u->name)) { |
77 | enumerate = true; |
78 | break; |
79 | } |
80 | } |
81 | |
82 | rc = alloc_chrdev_region(&mdev, 0, num_nodes, unit->name); |
83 | |
84 | if (rc) { |
85 | dev_warn(dev, "Failed to obtain major/minors" ); |
86 | goto fail_obtain; |
87 | } |
88 | |
89 | unit->major = MAJOR(mdev); |
90 | unit->lowest_minor = MINOR(mdev); |
91 | unit->num_nodes = num_nodes; |
92 | unit->private_data = private_data; |
93 | |
94 | unit->cdev = cdev_alloc(); |
95 | if (!unit->cdev) { |
96 | rc = -ENOMEM; |
97 | goto unregister_chrdev; |
98 | } |
99 | unit->cdev->ops = fops; |
100 | unit->cdev->owner = owner; |
101 | |
102 | rc = cdev_add(unit->cdev, MKDEV(unit->major, unit->lowest_minor), |
103 | unit->num_nodes); |
104 | if (rc) { |
105 | dev_err(dev, "Failed to add cdev.\n" ); |
106 | /* kobject_put() is normally done by cdev_del() */ |
107 | kobject_put(kobj: &unit->cdev->kobj); |
108 | goto unregister_chrdev; |
109 | } |
110 | |
111 | for (i = 0; i < num_nodes; i++) { |
112 | namelen = strnlen(p: idt, maxlen: len); |
113 | |
114 | if (namelen == len) { |
115 | dev_err(dev, "IDT's list of names is too short. This is exceptionally weird, because its CRC is OK\n" ); |
116 | rc = -ENODEV; |
117 | goto unroll_device_create; |
118 | } |
119 | |
120 | snprintf(buf: devname, size: sizeof(devname), fmt: "%s_%s" , |
121 | unit->name, idt); |
122 | |
123 | len -= namelen + 1; |
124 | idt += namelen + 1; |
125 | |
126 | device = device_create(cls: &xillybus_class, |
127 | NULL, |
128 | MKDEV(unit->major, |
129 | i + unit->lowest_minor), |
130 | NULL, |
131 | fmt: "%s" , devname); |
132 | |
133 | if (IS_ERR(ptr: device)) { |
134 | dev_err(dev, "Failed to create %s device. Aborting.\n" , |
135 | devname); |
136 | rc = -ENODEV; |
137 | goto unroll_device_create; |
138 | } |
139 | } |
140 | |
141 | if (len) { |
142 | dev_err(dev, "IDT's list of names is too long. This is exceptionally weird, because its CRC is OK\n" ); |
143 | rc = -ENODEV; |
144 | goto unroll_device_create; |
145 | } |
146 | |
147 | list_add_tail(new: &unit->list_entry, head: &unit_list); |
148 | |
149 | dev_info(dev, "Created %d device files.\n" , num_nodes); |
150 | |
151 | mutex_unlock(lock: &unit_mutex); |
152 | |
153 | return 0; |
154 | |
155 | unroll_device_create: |
156 | for (i--; i >= 0; i--) |
157 | device_destroy(cls: &xillybus_class, MKDEV(unit->major, |
158 | i + unit->lowest_minor)); |
159 | |
160 | cdev_del(unit->cdev); |
161 | |
162 | unregister_chrdev: |
163 | unregister_chrdev_region(MKDEV(unit->major, unit->lowest_minor), |
164 | unit->num_nodes); |
165 | |
166 | fail_obtain: |
167 | mutex_unlock(lock: &unit_mutex); |
168 | |
169 | kfree(objp: unit); |
170 | |
171 | return rc; |
172 | } |
173 | EXPORT_SYMBOL(xillybus_init_chrdev); |
174 | |
175 | void xillybus_cleanup_chrdev(void *private_data, |
176 | struct device *dev) |
177 | { |
178 | int minor; |
179 | struct xilly_unit *unit = NULL, *iter; |
180 | |
181 | mutex_lock(&unit_mutex); |
182 | |
183 | list_for_each_entry(iter, &unit_list, list_entry) |
184 | if (iter->private_data == private_data) { |
185 | unit = iter; |
186 | break; |
187 | } |
188 | |
189 | if (!unit) { |
190 | dev_err(dev, "Weird bug: Failed to find unit\n" ); |
191 | mutex_unlock(lock: &unit_mutex); |
192 | return; |
193 | } |
194 | |
195 | for (minor = unit->lowest_minor; |
196 | minor < (unit->lowest_minor + unit->num_nodes); |
197 | minor++) |
198 | device_destroy(cls: &xillybus_class, MKDEV(unit->major, minor)); |
199 | |
200 | cdev_del(unit->cdev); |
201 | |
202 | unregister_chrdev_region(MKDEV(unit->major, unit->lowest_minor), |
203 | unit->num_nodes); |
204 | |
205 | dev_info(dev, "Removed %d device files.\n" , |
206 | unit->num_nodes); |
207 | |
208 | list_del(entry: &unit->list_entry); |
209 | kfree(objp: unit); |
210 | |
211 | mutex_unlock(lock: &unit_mutex); |
212 | } |
213 | EXPORT_SYMBOL(xillybus_cleanup_chrdev); |
214 | |
215 | int xillybus_find_inode(struct inode *inode, |
216 | void **private_data, int *index) |
217 | { |
218 | int minor = iminor(inode); |
219 | int major = imajor(inode); |
220 | struct xilly_unit *unit = NULL, *iter; |
221 | |
222 | mutex_lock(&unit_mutex); |
223 | |
224 | list_for_each_entry(iter, &unit_list, list_entry) |
225 | if (iter->major == major && |
226 | minor >= iter->lowest_minor && |
227 | minor < (iter->lowest_minor + iter->num_nodes)) { |
228 | unit = iter; |
229 | break; |
230 | } |
231 | |
232 | if (!unit) { |
233 | mutex_unlock(lock: &unit_mutex); |
234 | return -ENODEV; |
235 | } |
236 | |
237 | *private_data = unit->private_data; |
238 | *index = minor - unit->lowest_minor; |
239 | |
240 | mutex_unlock(lock: &unit_mutex); |
241 | return 0; |
242 | } |
243 | EXPORT_SYMBOL(xillybus_find_inode); |
244 | |
245 | static int __init xillybus_class_init(void) |
246 | { |
247 | return class_register(class: &xillybus_class); |
248 | } |
249 | |
250 | static void __exit xillybus_class_exit(void) |
251 | { |
252 | class_unregister(class: &xillybus_class); |
253 | } |
254 | |
255 | module_init(xillybus_class_init); |
256 | module_exit(xillybus_class_exit); |
257 | |