0682e005d1
Platform drivers now have the option to have the platform core create and remove any needed sysfs attribute files. So take advantage of that and do not register "by hand" a sysfs group of attributes. Link: https://lore.kernel.org/r/20190731124349.4474-3-gregkh@linuxfoundation.org Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
479 lines
12 KiB
C
479 lines
12 KiB
C
// 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);
|
|
|
|
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 = 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, ®_br_new, ®_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);
|
|
|
|
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);
|
|
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;
|
|
|
|
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,
|
|
.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 <john.ogness@linutronix.de>");
|
|
MODULE_DESCRIPTION("Freescale Enhanced Local Bus Controller GPCM driver");
|