| // SPDX-License-Identifier: GPL-2.0 | 
 | /* uio_fsl_elbc_gpcm: UIO driver for eLBC/GPCM peripherals | 
 |  | 
 |    Copyright (C) 2014 Linutronix GmbH | 
 |      Author: John Ogness <[email protected]> | 
 |  | 
 |    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); | 
 |  | 
 | static DEVICE_ATTR(reg_br, 0664, reg_show, reg_store); | 
 | static DEVICE_ATTR(reg_or, 0664, reg_show, reg_store); | 
 |  | 
 | static struct attribute *uio_fsl_elbc_gpcm_attrs[] = { | 
 | 	&dev_attr_reg_br.attr, | 
 | 	&dev_attr_reg_or.attr, | 
 | 	NULL, | 
 | }; | 
 | ATTRIBUTE_GROUPS(uio_fsl_elbc_gpcm); | 
 |  | 
 | 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 = devm_kstrdup(priv->dev, 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 = devm_kzalloc(&pdev->dev, 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, ®_br_new, ®_or_new, | 
 | 			  &irq, &uio_name); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	/* allocate UIO structure */ | 
 | 	info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); | 
 | 	if (!info) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	/* 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"); | 
 | 			return -ENODEV; | 
 | 		} | 
 |  | 
 | 		/* 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"); | 
 | 		return -ENODEV; | 
 | 	} | 
 |  | 
 | 	/* set all UIO data */ | 
 | 	info->mem[0].name = devm_kasprintf(&pdev->dev, 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); | 
 |  | 
 | 	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_err2: | 
 | 	if (priv->shutdown) | 
 | 		priv->shutdown(info, true); | 
 | 	iounmap(info->mem[0].internal_addr); | 
 | 	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; | 
 |  | 
 | 	platform_set_drvdata(pdev, NULL); | 
 | 	uio_unregister_device(info); | 
 | 	if (priv->shutdown) | 
 | 		priv->shutdown(info, false); | 
 | 	iounmap(info->mem[0].internal_addr); | 
 |  | 
 | 	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, | 
 | 		.dev_groups = uio_fsl_elbc_gpcm_groups, | 
 | 	}, | 
 | 	.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 <[email protected]>"); | 
 | MODULE_DESCRIPTION("Freescale Enhanced Local Bus Controller GPCM driver"); |