1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* uio_fsl_elbc_gpcm: UIO driver for eLBC/GPCM peripherals |
3 | |
4 | Copyright (C) 2014 Linutronix GmbH |
5 | Author: John Ogness <john.ogness@linutronix.de> |
6 | |
7 | This driver provides UIO access to memory of a peripheral connected |
8 | to the Freescale enhanced local bus controller (eLBC) interface |
9 | using the general purpose chip-select mode (GPCM). |
10 | |
11 | Here is an example of the device tree entries: |
12 | |
13 | localbus@ffe05000 { |
14 | ranges = <0x2 0x0 0x0 0xff810000 0x10000>; |
15 | |
16 | dpm@2,0 { |
17 | compatible = "fsl,elbc-gpcm-uio"; |
18 | reg = <0x2 0x0 0x10000>; |
19 | elbc-gpcm-br = <0xff810800>; |
20 | elbc-gpcm-or = <0xffff09f7>; |
21 | interrupt-parent = <&mpic>; |
22 | interrupts = <4 1>; |
23 | device_type = "netx5152"; |
24 | uio_name = "netx_custom"; |
25 | netx5152,init-win0-offset = <0x0>; |
26 | }; |
27 | }; |
28 | |
29 | Only the entries reg (to identify bank) and elbc-gpcm-* (initial BR/OR |
30 | values) are required. The entries interrupt*, device_type, and uio_name |
31 | are optional (as well as any type-specific options such as |
32 | netx5152,init-win0-offset). As long as no interrupt handler is needed, |
33 | this driver can be used without any type-specific implementation. |
34 | |
35 | The netx5152 type has been tested to work with the netX 51/52 hardware |
36 | from Hilscher using the Hilscher userspace netX stack. |
37 | |
38 | The netx5152 type should serve as a model to add new type-specific |
39 | devices as needed. |
40 | */ |
41 | |
42 | #include <linux/module.h> |
43 | #include <linux/device.h> |
44 | #include <linux/string.h> |
45 | #include <linux/slab.h> |
46 | #include <linux/platform_device.h> |
47 | #include <linux/uio_driver.h> |
48 | #include <linux/of_address.h> |
49 | #include <linux/of_irq.h> |
50 | |
51 | #include <asm/fsl_lbc.h> |
52 | |
53 | #define MAX_BANKS 8 |
54 | |
55 | struct fsl_elbc_gpcm { |
56 | struct device *dev; |
57 | struct fsl_lbc_regs __iomem *lbc; |
58 | u32 bank; |
59 | const char *name; |
60 | |
61 | void (*init)(struct uio_info *info); |
62 | void (*shutdown)(struct uio_info *info, bool init_err); |
63 | irqreturn_t (*irq_handler)(int irq, struct uio_info *info); |
64 | }; |
65 | |
66 | static ssize_t reg_show(struct device *dev, struct device_attribute *attr, |
67 | char *buf); |
68 | static ssize_t reg_store(struct device *dev, struct device_attribute *attr, |
69 | const char *buf, size_t count); |
70 | |
71 | static DEVICE_ATTR(reg_br, 0664, reg_show, reg_store); |
72 | static DEVICE_ATTR(reg_or, 0664, reg_show, reg_store); |
73 | |
74 | static struct attribute *uio_fsl_elbc_gpcm_attrs[] = { |
75 | &dev_attr_reg_br.attr, |
76 | &dev_attr_reg_or.attr, |
77 | NULL, |
78 | }; |
79 | ATTRIBUTE_GROUPS(uio_fsl_elbc_gpcm); |
80 | |
81 | static ssize_t reg_show(struct device *dev, struct device_attribute *attr, |
82 | char *buf) |
83 | { |
84 | struct uio_info *info = dev_get_drvdata(dev); |
85 | struct fsl_elbc_gpcm *priv = info->priv; |
86 | struct fsl_lbc_bank *bank = &priv->lbc->bank[priv->bank]; |
87 | |
88 | if (attr == &dev_attr_reg_br) { |
89 | return scnprintf(buf, PAGE_SIZE, fmt: "0x%08x\n" , |
90 | in_be32(&bank->br)); |
91 | |
92 | } else if (attr == &dev_attr_reg_or) { |
93 | return scnprintf(buf, PAGE_SIZE, fmt: "0x%08x\n" , |
94 | in_be32(&bank->or)); |
95 | } |
96 | |
97 | return 0; |
98 | } |
99 | |
100 | static ssize_t reg_store(struct device *dev, struct device_attribute *attr, |
101 | const char *buf, size_t count) |
102 | { |
103 | struct uio_info *info = dev_get_drvdata(dev); |
104 | struct fsl_elbc_gpcm *priv = info->priv; |
105 | struct fsl_lbc_bank *bank = &priv->lbc->bank[priv->bank]; |
106 | unsigned long val; |
107 | u32 reg_br_cur; |
108 | u32 reg_or_cur; |
109 | u32 reg_new; |
110 | |
111 | /* parse use input */ |
112 | if (kstrtoul(s: buf, base: 0, res: &val) != 0) |
113 | return -EINVAL; |
114 | reg_new = (u32)val; |
115 | |
116 | /* read current values */ |
117 | reg_br_cur = in_be32(&bank->br); |
118 | reg_or_cur = in_be32(&bank->or); |
119 | |
120 | if (attr == &dev_attr_reg_br) { |
121 | /* not allowed to change effective base address */ |
122 | if ((reg_br_cur & reg_or_cur & BR_BA) != |
123 | (reg_new & reg_or_cur & BR_BA)) { |
124 | return -EINVAL; |
125 | } |
126 | |
127 | /* not allowed to change mode */ |
128 | if ((reg_new & BR_MSEL) != BR_MS_GPCM) |
129 | return -EINVAL; |
130 | |
131 | /* write new value (force valid) */ |
132 | out_be32(&bank->br, reg_new | BR_V); |
133 | |
134 | } else if (attr == &dev_attr_reg_or) { |
135 | /* not allowed to change access mask */ |
136 | if ((reg_or_cur & OR_GPCM_AM) != (reg_new & OR_GPCM_AM)) |
137 | return -EINVAL; |
138 | |
139 | /* write new value */ |
140 | out_be32(&bank->or, reg_new); |
141 | |
142 | } else { |
143 | return -EINVAL; |
144 | } |
145 | |
146 | return count; |
147 | } |
148 | |
149 | #ifdef CONFIG_UIO_FSL_ELBC_GPCM_NETX5152 |
150 | #define DPM_HOST_WIN0_OFFSET 0xff00 |
151 | #define DPM_HOST_INT_STAT0 0xe0 |
152 | #define DPM_HOST_INT_EN0 0xf0 |
153 | #define DPM_HOST_INT_MASK 0xe600ffff |
154 | #define DPM_HOST_INT_GLOBAL_EN 0x80000000 |
155 | |
156 | static irqreturn_t netx5152_irq_handler(int irq, struct uio_info *info) |
157 | { |
158 | void __iomem *reg_int_en = info->mem[0].internal_addr + |
159 | DPM_HOST_WIN0_OFFSET + |
160 | DPM_HOST_INT_EN0; |
161 | void __iomem *reg_int_stat = info->mem[0].internal_addr + |
162 | DPM_HOST_WIN0_OFFSET + |
163 | DPM_HOST_INT_STAT0; |
164 | |
165 | /* check if an interrupt is enabled and active */ |
166 | if ((ioread32(reg_int_en) & ioread32(reg_int_stat) & |
167 | DPM_HOST_INT_MASK) == 0) { |
168 | return IRQ_NONE; |
169 | } |
170 | |
171 | /* disable interrupts */ |
172 | iowrite32(ioread32(reg_int_en) & ~DPM_HOST_INT_GLOBAL_EN, reg_int_en); |
173 | |
174 | return IRQ_HANDLED; |
175 | } |
176 | |
177 | static void netx5152_init(struct uio_info *info) |
178 | { |
179 | unsigned long win0_offset = DPM_HOST_WIN0_OFFSET; |
180 | struct fsl_elbc_gpcm *priv = info->priv; |
181 | const void *prop; |
182 | |
183 | /* get an optional initial win0 offset */ |
184 | prop = of_get_property(priv->dev->of_node, |
185 | "netx5152,init-win0-offset" , NULL); |
186 | if (prop) |
187 | win0_offset = of_read_ulong(prop, 1); |
188 | |
189 | /* disable interrupts */ |
190 | iowrite32(0, info->mem[0].internal_addr + win0_offset + |
191 | DPM_HOST_INT_EN0); |
192 | } |
193 | |
194 | static void netx5152_shutdown(struct uio_info *info, bool init_err) |
195 | { |
196 | if (init_err) |
197 | return; |
198 | |
199 | /* disable interrupts */ |
200 | iowrite32(0, info->mem[0].internal_addr + DPM_HOST_WIN0_OFFSET + |
201 | DPM_HOST_INT_EN0); |
202 | } |
203 | #endif |
204 | |
205 | static void setup_periph(struct fsl_elbc_gpcm *priv, |
206 | const char *type) |
207 | { |
208 | #ifdef CONFIG_UIO_FSL_ELBC_GPCM_NETX5152 |
209 | if (strcmp(type, "netx5152" ) == 0) { |
210 | priv->irq_handler = netx5152_irq_handler; |
211 | priv->init = netx5152_init; |
212 | priv->shutdown = netx5152_shutdown; |
213 | priv->name = "netX 51/52" ; |
214 | return; |
215 | } |
216 | #endif |
217 | } |
218 | |
219 | static int check_of_data(struct fsl_elbc_gpcm *priv, |
220 | struct resource *res, |
221 | u32 reg_br, u32 reg_or) |
222 | { |
223 | /* check specified bank */ |
224 | if (priv->bank >= MAX_BANKS) { |
225 | dev_err(priv->dev, "invalid bank\n" ); |
226 | return -ENODEV; |
227 | } |
228 | |
229 | /* check specified mode (BR_MS_GPCM is 0) */ |
230 | if ((reg_br & BR_MSEL) != BR_MS_GPCM) { |
231 | dev_err(priv->dev, "unsupported mode\n" ); |
232 | return -ENODEV; |
233 | } |
234 | |
235 | /* check specified mask vs. resource size */ |
236 | if ((~(reg_or & OR_GPCM_AM) + 1) != resource_size(res)) { |
237 | dev_err(priv->dev, "address mask / size mismatch\n" ); |
238 | return -ENODEV; |
239 | } |
240 | |
241 | /* check specified address */ |
242 | if ((reg_br & reg_or & BR_BA) != fsl_lbc_addr(res->start)) { |
243 | dev_err(priv->dev, "base address mismatch\n" ); |
244 | return -ENODEV; |
245 | } |
246 | |
247 | return 0; |
248 | } |
249 | |
250 | static int get_of_data(struct fsl_elbc_gpcm *priv, struct device_node *node, |
251 | struct resource *res, u32 *reg_br, |
252 | u32 *reg_or, unsigned int *irq, char **name) |
253 | { |
254 | const char *dt_name; |
255 | const char *type; |
256 | int ret; |
257 | |
258 | /* get the memory resource */ |
259 | ret = of_address_to_resource(dev: node, index: 0, r: res); |
260 | if (ret) { |
261 | dev_err(priv->dev, "failed to get resource\n" ); |
262 | return ret; |
263 | } |
264 | |
265 | /* get the bank number */ |
266 | ret = of_property_read_u32(np: node, propname: "reg" , out_value: &priv->bank); |
267 | if (ret) { |
268 | dev_err(priv->dev, "failed to get bank number\n" ); |
269 | return ret; |
270 | } |
271 | |
272 | /* get BR value to set */ |
273 | ret = of_property_read_u32(np: node, propname: "elbc-gpcm-br" , out_value: reg_br); |
274 | if (ret) { |
275 | dev_err(priv->dev, "missing elbc-gpcm-br value\n" ); |
276 | return ret; |
277 | } |
278 | |
279 | /* get OR value to set */ |
280 | ret = of_property_read_u32(np: node, propname: "elbc-gpcm-or" , out_value: reg_or); |
281 | if (ret) { |
282 | dev_err(priv->dev, "missing elbc-gpcm-or value\n" ); |
283 | return ret; |
284 | } |
285 | |
286 | /* get optional peripheral type */ |
287 | priv->name = "generic" ; |
288 | if (of_property_read_string(np: node, propname: "device_type" , out_string: &type) == 0) |
289 | setup_periph(priv, type); |
290 | |
291 | /* get optional irq value */ |
292 | *irq = irq_of_parse_and_map(node, index: 0); |
293 | |
294 | /* sanity check device tree data */ |
295 | ret = check_of_data(priv, res, reg_br: *reg_br, reg_or: *reg_or); |
296 | if (ret) |
297 | return ret; |
298 | |
299 | /* get optional uio name */ |
300 | if (of_property_read_string(np: node, propname: "uio_name" , out_string: &dt_name) != 0) |
301 | dt_name = "eLBC_GPCM" ; |
302 | *name = devm_kstrdup(dev: priv->dev, s: dt_name, GFP_KERNEL); |
303 | if (!*name) |
304 | return -ENOMEM; |
305 | |
306 | return 0; |
307 | } |
308 | |
309 | static int uio_fsl_elbc_gpcm_probe(struct platform_device *pdev) |
310 | { |
311 | struct device_node *node = pdev->dev.of_node; |
312 | struct fsl_elbc_gpcm *priv; |
313 | struct uio_info *info; |
314 | char *uio_name = NULL; |
315 | struct resource res; |
316 | unsigned int irq; |
317 | u32 reg_br_cur; |
318 | u32 reg_or_cur; |
319 | u32 reg_br_new; |
320 | u32 reg_or_new; |
321 | int ret; |
322 | |
323 | if (!fsl_lbc_ctrl_dev || !fsl_lbc_ctrl_dev->regs) |
324 | return -ENODEV; |
325 | |
326 | /* allocate private data */ |
327 | priv = devm_kzalloc(dev: &pdev->dev, size: sizeof(*priv), GFP_KERNEL); |
328 | if (!priv) |
329 | return -ENOMEM; |
330 | priv->dev = &pdev->dev; |
331 | priv->lbc = fsl_lbc_ctrl_dev->regs; |
332 | |
333 | /* get device tree data */ |
334 | ret = get_of_data(priv, node, res: &res, reg_br: ®_br_new, reg_or: ®_or_new, |
335 | irq: &irq, name: &uio_name); |
336 | if (ret) |
337 | return ret; |
338 | |
339 | /* allocate UIO structure */ |
340 | info = devm_kzalloc(dev: &pdev->dev, size: sizeof(*info), GFP_KERNEL); |
341 | if (!info) |
342 | return -ENOMEM; |
343 | |
344 | /* get current BR/OR values */ |
345 | reg_br_cur = in_be32(&priv->lbc->bank[priv->bank].br); |
346 | reg_or_cur = in_be32(&priv->lbc->bank[priv->bank].or); |
347 | |
348 | /* if bank already configured, make sure it matches */ |
349 | if ((reg_br_cur & BR_V)) { |
350 | if ((reg_br_cur & BR_MSEL) != BR_MS_GPCM || |
351 | (reg_br_cur & reg_or_cur & BR_BA) |
352 | != fsl_lbc_addr(res.start)) { |
353 | dev_err(priv->dev, |
354 | "bank in use by another peripheral\n" ); |
355 | return -ENODEV; |
356 | } |
357 | |
358 | /* warn if behavior settings changing */ |
359 | if ((reg_br_cur & ~(BR_BA | BR_V)) != |
360 | (reg_br_new & ~(BR_BA | BR_V))) { |
361 | dev_warn(priv->dev, |
362 | "modifying BR settings: 0x%08x -> 0x%08x" , |
363 | reg_br_cur, reg_br_new); |
364 | } |
365 | if ((reg_or_cur & ~OR_GPCM_AM) != (reg_or_new & ~OR_GPCM_AM)) { |
366 | dev_warn(priv->dev, |
367 | "modifying OR settings: 0x%08x -> 0x%08x" , |
368 | reg_or_cur, reg_or_new); |
369 | } |
370 | } |
371 | |
372 | /* configure the bank (force base address and GPCM) */ |
373 | reg_br_new &= ~(BR_BA | BR_MSEL); |
374 | reg_br_new |= fsl_lbc_addr(res.start) | BR_MS_GPCM | BR_V; |
375 | out_be32(&priv->lbc->bank[priv->bank].or, reg_or_new); |
376 | out_be32(&priv->lbc->bank[priv->bank].br, reg_br_new); |
377 | |
378 | /* map the memory resource */ |
379 | info->mem[0].internal_addr = ioremap(offset: res.start, size: resource_size(res: &res)); |
380 | if (!info->mem[0].internal_addr) { |
381 | dev_err(priv->dev, "failed to map chip region\n" ); |
382 | return -ENODEV; |
383 | } |
384 | |
385 | /* set all UIO data */ |
386 | info->mem[0].name = devm_kasprintf(dev: &pdev->dev, GFP_KERNEL, fmt: "%pOFn" , node); |
387 | info->mem[0].addr = res.start; |
388 | info->mem[0].size = resource_size(res: &res); |
389 | info->mem[0].memtype = UIO_MEM_PHYS; |
390 | info->priv = priv; |
391 | info->name = uio_name; |
392 | info->version = "0.0.1" ; |
393 | if (irq) { |
394 | if (priv->irq_handler) { |
395 | info->irq = irq; |
396 | info->irq_flags = IRQF_SHARED; |
397 | info->handler = priv->irq_handler; |
398 | } else { |
399 | irq = 0; |
400 | dev_warn(priv->dev, "ignoring irq, no handler\n" ); |
401 | } |
402 | } |
403 | |
404 | if (priv->init) |
405 | priv->init(info); |
406 | |
407 | /* register UIO device */ |
408 | if (uio_register_device(priv->dev, info) != 0) { |
409 | dev_err(priv->dev, "UIO registration failed\n" ); |
410 | ret = -ENODEV; |
411 | goto out_err2; |
412 | } |
413 | |
414 | /* store private data */ |
415 | platform_set_drvdata(pdev, data: info); |
416 | |
417 | dev_info(priv->dev, |
418 | "eLBC/GPCM device (%s) at 0x%llx, bank %d, irq=%d\n" , |
419 | priv->name, (unsigned long long)res.start, priv->bank, |
420 | irq ? : -1); |
421 | |
422 | return 0; |
423 | out_err2: |
424 | if (priv->shutdown) |
425 | priv->shutdown(info, true); |
426 | iounmap(addr: info->mem[0].internal_addr); |
427 | return ret; |
428 | } |
429 | |
430 | static int uio_fsl_elbc_gpcm_remove(struct platform_device *pdev) |
431 | { |
432 | struct uio_info *info = platform_get_drvdata(pdev); |
433 | struct fsl_elbc_gpcm *priv = info->priv; |
434 | |
435 | platform_set_drvdata(pdev, NULL); |
436 | uio_unregister_device(info); |
437 | if (priv->shutdown) |
438 | priv->shutdown(info, false); |
439 | iounmap(addr: info->mem[0].internal_addr); |
440 | |
441 | return 0; |
442 | |
443 | } |
444 | |
445 | static const struct of_device_id uio_fsl_elbc_gpcm_match[] = { |
446 | { .compatible = "fsl,elbc-gpcm-uio" , }, |
447 | {} |
448 | }; |
449 | MODULE_DEVICE_TABLE(of, uio_fsl_elbc_gpcm_match); |
450 | |
451 | static struct platform_driver uio_fsl_elbc_gpcm_driver = { |
452 | .driver = { |
453 | .name = "fsl,elbc-gpcm-uio" , |
454 | .of_match_table = uio_fsl_elbc_gpcm_match, |
455 | .dev_groups = uio_fsl_elbc_gpcm_groups, |
456 | }, |
457 | .probe = uio_fsl_elbc_gpcm_probe, |
458 | .remove = uio_fsl_elbc_gpcm_remove, |
459 | }; |
460 | module_platform_driver(uio_fsl_elbc_gpcm_driver); |
461 | |
462 | MODULE_LICENSE("GPL" ); |
463 | MODULE_AUTHOR("John Ogness <john.ogness@linutronix.de>" ); |
464 | MODULE_DESCRIPTION("Freescale Enhanced Local Bus Controller GPCM driver" ); |
465 | |