summaryrefslogtreecommitdiffstats
path: root/drivers/uio/uio_fsl_elbc_gpcm.c
blob: 0ee3cd3c25ee28fdfec614d7f2f563c64f7b5873 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
// SPDX-License-Identifier: GPL-2.0
/* uio_fsl_elbc_gpcm: UIO driver for eLBC/GPCM peripherals

   Copyright (C) 2014 Linutronix GmbH
     Author: John Ogness <john.ogness@linutronix.de>

   This driver provides UIO access to memory of a peripheral connected
   to the Freescale enhanced local bus controller (eLBC) interface
   using the general purpose chip-select mode (GPCM).

   Here is an example of the device tree entries:

	localbus@ffe05000 {
		ranges = <0x2 0x0 0x0 0xff810000 0x10000>;

		dpm@2,0 {
			compatible = "fsl,elbc-gpcm-uio";
			reg = <0x2 0x0 0x10000>;
			elbc-gpcm-br = <0xff810800>;
			elbc-gpcm-or = <0xffff09f7>;
			interrupt-parent = <&mpic>;
			interrupts = <4 1>;
			device_type = "netx5152";
			uio_name = "netx_custom";
			netx5152,init-win0-offset = <0x0>;
		};
	};

   Only the entries reg (to identify bank) and elbc-gpcm-* (initial BR/OR
   values) are required. The entries interrupt*, device_type, and uio_name
   are optional (as well as any type-specific options such as
   netx5152,init-win0-offset). As long as no interrupt handler is needed,
   this driver can be used without any type-specific implementation.

   The netx5152 type has been tested to work with the netX 51/52 hardware
   from Hilscher using the Hilscher userspace netX stack.

   The netx5152 type should serve as a model to add new type-specific
   devices as needed.
*/

#include <linux/module.h>
#include <linux/device.h>
#include <linux/string.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/uio_driver.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>

#include <asm/fsl_lbc.h>

#define MAX_BANKS 8

struct fsl_elbc_gpcm {
	struct device *dev;
	struct fsl_lbc_regs __iomem *lbc;
	u32 bank;
	const char *name;

	void (*init)(struct uio_info *info);
	void (*shutdown)(struct uio_info *info, bool init_err);
	irqreturn_t (*irq_handler)(int irq, struct uio_info *info);
};

static ssize_t reg_show(struct device *dev, struct device_attribute *attr,
			char *buf);
static ssize_t reg_store(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count);

DEVICE_ATTR(reg_br, S_IRUGO|S_IWUSR|S_IWGRP, reg_show, reg_store);
DEVICE_ATTR(reg_or, S_IRUGO|S_IWUSR|S_IWGRP, reg_show, reg_store);

static ssize_t reg_show(struct device *dev, struct device_attribute *attr,
			char *buf)
{
	struct uio_info *info = dev_get_drvdata(dev);
	struct fsl_elbc_gpcm *priv = info->priv;
	struct fsl_lbc_bank *bank = &priv->lbc->bank[priv->bank];

	if (attr == &dev_attr_reg_br) {
		return scnprintf(buf, PAGE_SIZE, "0x%08x\n",
				 in_be32(&bank->br));

	} else if (attr == &dev_attr_reg_or) {
		return scnprintf(buf, PAGE_SIZE, "0x%08x\n",
				 in_be32(&bank->or));
	}

	return 0;
}

static ssize_t reg_store(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count)
{
	struct uio_info *info = dev_get_drvdata(dev);
	struct fsl_elbc_gpcm *priv = info->priv;
	struct fsl_lbc_bank *bank = &priv->lbc->bank[priv->bank];
	unsigned long val;
	u32 reg_br_cur;
	u32 reg_or_cur;
	u32 reg_new;

	/* parse use input */
	if (kstrtoul(buf, 0, &val) != 0)
		return -EINVAL;
	reg_new = (u32)val;

	/* read current values */
	reg_br_cur = in_be32(&bank->br);
	reg_or_cur = in_be32(&bank->or);

	if (attr == &dev_attr_reg_br) {
		/* not allowed to change effective base address */
		if ((reg_br_cur & reg_or_cur & BR_BA) !=
		    (reg_new & reg_or_cur & BR_BA)) {
			return -EINVAL;
		}

		/* not allowed to change mode */
		if ((reg_new & BR_MSEL) != BR_MS_GPCM)
			return -EINVAL;

		/* write new value (force valid) */
		out_be32(&bank->br, reg_new | BR_V);

	} else if (attr == &dev_attr_reg_or) {
		/* not allowed to change access mask */
		if ((reg_or_cur & OR_GPCM_AM) != (reg_new & OR_GPCM_AM))
			return -EINVAL;

		/* write new value */
		out_be32(&bank->or, reg_new);

	} else {
		return -EINVAL;
	}

	return count;
}

#ifdef CONFIG_UIO_FSL_ELBC_GPCM_NETX5152
#define DPM_HOST_WIN0_OFFSET	0xff00
#define DPM_HOST_INT_STAT0	0xe0
#define DPM_HOST_INT_EN0	0xf0
#define DPM_HOST_INT_MASK	0xe600ffff
#define DPM_HOST_INT_GLOBAL_EN	0x80000000

static irqreturn_t netx5152_irq_handler(int irq, struct uio_info *info)
{
	void __iomem *reg_int_en = info->mem[0].internal_addr +
					DPM_HOST_WIN0_OFFSET +
					DPM_HOST_INT_EN0;
	void __iomem *reg_int_stat = info->mem[0].internal_addr +
					DPM_HOST_WIN0_OFFSET +
					DPM_HOST_INT_STAT0;

	/* check if an interrupt is enabled and active */
	if ((ioread32(reg_int_en) & ioread32(reg_int_stat) &
	     DPM_HOST_INT_MASK) == 0) {
		return IRQ_NONE;
	}

	/* disable interrupts */
	iowrite32(ioread32(reg_int_en) & ~DPM_HOST_INT_GLOBAL_EN, reg_int_en);

	return IRQ_HANDLED;
}

static void netx5152_init(struct uio_info *info)
{
	unsigned long win0_offset = DPM_HOST_WIN0_OFFSET;
	struct fsl_elbc_gpcm *priv = info->priv;
	const void *prop;

	/* get an optional initial win0 offset */
	prop = of_get_property(priv->dev->of_node,
			       "netx5152,init-win0-offset", NULL);
	if (prop)
		win0_offset = of_read_ulong(prop, 1);

	/* disable interrupts */
	iowrite32(0, info->mem[0].internal_addr + win0_offset +
		     DPM_HOST_INT_EN0);
}

static void netx5152_shutdown(struct uio_info *info, bool init_err)
{
	if (init_err)
		return;

	/* disable interrupts */
	iowrite32(0, info->mem[0].internal_addr + DPM_HOST_WIN0_OFFSET +
		     DPM_HOST_INT_EN0);
}
#endif

static void setup_periph(struct fsl_elbc_gpcm *priv,
				   const char *type)
{
#ifdef CONFIG_UIO_FSL_ELBC_GPCM_NETX5152
	if (strcmp(type, "netx5152") == 0) {
		priv->irq_handler = netx5152_irq_handler;
		priv->init = netx5152_init;
		priv->shutdown = netx5152_shutdown;
		priv->name = "netX 51/52";
		return;
	}
#endif
}

static int check_of_data(struct fsl_elbc_gpcm *priv,
				   struct resource *res,
				   u32 reg_br, u32 reg_or)
{
	/* check specified bank */
	if (priv->bank >= MAX_BANKS) {
		dev_err(priv->dev, "invalid bank\n");
		return -ENODEV;
	}

	/* check specified mode (BR_MS_GPCM is 0) */
	if ((reg_br & BR_MSEL) != BR_MS_GPCM) {
		dev_err(priv->dev, "unsupported mode\n");
		return -ENODEV;
	}

	/* check specified mask vs. resource size */
	if ((~(reg_or & OR_GPCM_AM) + 1) != resource_size(res)) {
		dev_err(priv->dev, "address mask / size mismatch\n");
		return -ENODEV;
	}

	/* check specified address */
	if ((reg_br & reg_or & BR_BA) != fsl_lbc_addr(res->start)) {
		dev_err(priv->dev, "base address mismatch\n");
		return -ENODEV;
	}

	return 0;
}

static int get_of_data(struct fsl_elbc_gpcm *priv, struct device_node *node,
		       struct resource *res, u32 *reg_br,
		       u32 *reg_or, unsigned int *irq, char **name)
{
	const char *dt_name;
	const char *type;
	int ret;

	/* get the memory resource */
	ret = of_address_to_resource(node, 0, res);
	if (ret) {
		dev_err(priv->dev, "failed to get resource\n");
		return ret;
	}

	/* get the bank number */
	ret = of_property_read_u32(node, "reg", &priv->bank);
	if (ret) {
		dev_err(priv->dev, "failed to get bank number\n");
		return ret;
	}

	/* get BR value to set */
	ret = of_property_read_u32(node, "elbc-gpcm-br", reg_br);
	if (ret) {
		dev_err(priv->dev, "missing elbc-gpcm-br value\n");
		return ret;
	}

	/* get OR value to set */
	ret = of_property_read_u32(node, "elbc-gpcm-or", reg_or);
	if (ret) {
		dev_err(priv->dev, "missing elbc-gpcm-or value\n");
		return ret;
	}

	/* get optional peripheral type */
	priv->name = "generic";
	if (of_property_read_string(node, "device_type", &type) == 0)
		setup_periph(priv, type);

	/* get optional irq value */
	*irq = irq_of_parse_and_map(node, 0);

	/* sanity check device tree data */
	ret = check_of_data(priv, res, *reg_br, *reg_or);
	if (ret)
		return ret;

	/* get optional uio name */
	if (of_property_read_string(node, "uio_name", &dt_name) != 0)
		dt_name = "eLBC_GPCM";
	*name = kstrdup(dt_name, GFP_KERNEL);
	if (!*name)
		return -ENOMEM;

	return 0;
}

static int uio_fsl_elbc_gpcm_probe(struct platform_device *pdev)
{
	struct device_node *node = pdev->dev.of_node;
	struct fsl_elbc_gpcm *priv;
	struct uio_info *info;
	char *uio_name = NULL;
	struct resource res;
	unsigned int irq;
	u32 reg_br_cur;
	u32 reg_or_cur;
	u32 reg_br_new;
	u32 reg_or_new;
	int ret;

	if (!fsl_lbc_ctrl_dev || !fsl_lbc_ctrl_dev->regs)
		return -ENODEV;

	/* allocate private data */
	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	priv->dev = &pdev->dev;
	priv->lbc = fsl_lbc_ctrl_dev->regs;

	/* get device tree data */
	ret = get_of_data(priv, node, &res, &reg_br_new, &reg_or_new,
			  &irq, &uio_name);
	if (ret)
		goto out_err0;

	/* allocate UIO structure */
	info = kzalloc(sizeof(*info), GFP_KERNEL);
	if (!info) {
		ret = -ENOMEM;
		goto out_err0;
	}

	/* get current BR/OR values */
	reg_br_cur = in_be32(&priv->lbc->bank[priv->bank].br);
	reg_or_cur = in_be32(&priv->lbc->bank[priv->bank].or);

	/* if bank already configured, make sure it matches */
	if ((reg_br_cur & BR_V)) {
		if ((reg_br_cur & BR_MSEL) != BR_MS_GPCM ||
		    (reg_br_cur & reg_or_cur & BR_BA)
		     != fsl_lbc_addr(res.start)) {
			dev_err(priv->dev,
				"bank in use by another peripheral\n");
			ret = -ENODEV;
			goto out_err1;
		}

		/* warn if behavior settings changing */
		if ((reg_br_cur & ~(BR_BA | BR_V)) !=
		    (reg_br_new & ~(BR_BA | BR_V))) {
			dev_warn(priv->dev,
				 "modifying BR settings: 0x%08x -> 0x%08x",
				 reg_br_cur, reg_br_new);
		}
		if ((reg_or_cur & ~OR_GPCM_AM) != (reg_or_new & ~OR_GPCM_AM)) {
			dev_warn(priv->dev,
				 "modifying OR settings: 0x%08x -> 0x%08x",
				 reg_or_cur, reg_or_new);
		}
	}

	/* configure the bank (force base address and GPCM) */
	reg_br_new &= ~(BR_BA | BR_MSEL);
	reg_br_new |= fsl_lbc_addr(res.start) | BR_MS_GPCM | BR_V;
	out_be32(&priv->lbc->bank[priv->bank].or, reg_or_new);
	out_be32(&priv->lbc->bank[priv->bank].br, reg_br_new);

	/* map the memory resource */
	info->mem[0].internal_addr = ioremap(res.start, resource_size(&res));
	if (!info->mem[0].internal_addr) {
		dev_err(priv->dev, "failed to map chip region\n");
		ret = -ENODEV;
		goto out_err1;
	}

	/* set all UIO data */
	info->mem[0].name = kasprintf(GFP_KERNEL, "%pOFn", node);
	info->mem[0].addr = res.start;
	info->mem[0].size = resource_size(&res);
	info->mem[0].memtype = UIO_MEM_PHYS;
	info->priv = priv;
	info->name = uio_name;
	info->version = "0.0.1";
	if (irq != NO_IRQ) {
		if (priv->irq_handler) {
			info->irq = irq;
			info->irq_flags = IRQF_SHARED;
			info->handler = priv->irq_handler;
		} else {
			irq = NO_IRQ;
			dev_warn(priv->dev, "ignoring irq, no handler\n");
		}
	}

	if (priv->init)
		priv->init(info);

	/* register UIO device */
	if (uio_register_device(priv->dev, info) != 0) {
		dev_err(priv->dev, "UIO registration failed\n");
		ret = -ENODEV;
		goto out_err2;
	}

	/* store private data */
	platform_set_drvdata(pdev, info);

	/* create sysfs files */
	ret = device_create_file(priv->dev, &dev_attr_reg_br);
	if (ret)
		goto out_err3;
	ret = device_create_file(priv->dev, &dev_attr_reg_or);
	if (ret)
		goto out_err4;

	dev_info(priv->dev,
		 "eLBC/GPCM device (%s) at 0x%llx, bank %d, irq=%d\n",
		 priv->name, (unsigned long long)res.start, priv->bank,
		 irq != NO_IRQ ? irq : -1);

	return 0;
out_err4:
	device_remove_file(priv->dev, &dev_attr_reg_br);
out_err3:
	platform_set_drvdata(pdev, NULL);
	uio_unregister_device(info);
out_err2:
	if (priv->shutdown)
		priv->shutdown(info, true);
	iounmap(info->mem[0].internal_addr);
out_err1:
	kfree(info->mem[0].name);
	kfree(info);
out_err0:
	kfree(uio_name);
	kfree(priv);
	return ret;
}

static int uio_fsl_elbc_gpcm_remove(struct platform_device *pdev)
{
	struct uio_info *info = platform_get_drvdata(pdev);
	struct fsl_elbc_gpcm *priv = info->priv;

	device_remove_file(priv->dev, &dev_attr_reg_or);
	device_remove_file(priv->dev, &dev_attr_reg_br);
	platform_set_drvdata(pdev, NULL);
	uio_unregister_device(info);
	if (priv->shutdown)
		priv->shutdown(info, false);
	iounmap(info->mem[0].internal_addr);
	kfree(info->mem[0].name);
	kfree(info->name);
	kfree(info);
	kfree(priv);

	return 0;

}

static const struct of_device_id uio_fsl_elbc_gpcm_match[] = {
	{ .compatible = "fsl,elbc-gpcm-uio", },
	{}
};
MODULE_DEVICE_TABLE(of, uio_fsl_elbc_gpcm_match);

static struct platform_driver uio_fsl_elbc_gpcm_driver = {
	.driver = {
		.name = "fsl,elbc-gpcm-uio",
		.of_match_table = uio_fsl_elbc_gpcm_match,
	},
	.probe = uio_fsl_elbc_gpcm_probe,
	.remove = uio_fsl_elbc_gpcm_remove,
};
module_platform_driver(uio_fsl_elbc_gpcm_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Ogness <john.ogness@linutronix.de>");
MODULE_DESCRIPTION("Freescale Enhanced Local Bus Controller GPCM driver");