-
Notifications
You must be signed in to change notification settings - Fork 5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
drivers: char: add generic gpiomem driver
Based on bcm2835-gpiomem. We allow export of the "GPIO registers" to userspace via a chardev as this allows for finer access control (e.g. users must be group gpio, root not required). This driver allows access to either rp1-gpiomem or gpiomem, depending on which nodes are populated in devicetree. RP1 has a different look-and-feel to BCM283x SoCs as it has split ranges for IO controls and the parallel registered OE/IN/OUT access. To handle this, the driver concatenates the ranges for an IO bank and the corresponding RIO instance into a contiguous buffer. Signed-off-by: Jonathan Bell <jonathan@raspberrypi.com>
- Loading branch information
1 parent
65b180b
commit 068ef79
Showing
3 changed files
with
284 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause | ||
/** | ||
* raspberrypi-gpiomem.c | ||
* | ||
* Provides MMIO access to discontiguous section of Device memory as a linear | ||
* user mapping. Successor to bcm2835-gpiomem.c. | ||
* | ||
* Copyright (c) 2023, Raspberry Pi Ltd. | ||
*/ | ||
|
||
#include <linux/kernel.h> | ||
#include <linux/module.h> | ||
#include <linux/of.h> | ||
#include <linux/of_device.h> | ||
#include <linux/platform_device.h> | ||
#include <linux/mm.h> | ||
#include <linux/slab.h> | ||
#include <linux/cdev.h> | ||
#include <linux/pagemap.h> | ||
#include <linux/io.h> | ||
|
||
#define DRIVER_NAME "rpi-gpiomem" | ||
#define DEVICE_MINOR 0 | ||
|
||
/* | ||
* Sensible max for a hypothetical "gpio" controller that splits pads, | ||
* IO controls, GPIO in/out/enable, and function selection into different | ||
* ranges. Most use only one or two. | ||
*/ | ||
#define MAX_RANGES 4 | ||
|
||
struct io_windows { | ||
unsigned long phys_base; | ||
unsigned long len; | ||
}; | ||
|
||
struct rpi_gpiomem_priv { | ||
dev_t devid; | ||
struct class *class; | ||
struct cdev rpi_gpiomem_cdev; | ||
struct device *dev; | ||
const char *name; | ||
unsigned int nr_wins; | ||
struct io_windows iowins[4]; | ||
}; | ||
|
||
static int rpi_gpiomem_open(struct inode *inode, struct file *file) | ||
{ | ||
int dev = iminor(inode); | ||
int ret = 0; | ||
struct rpi_gpiomem_priv *priv; | ||
|
||
if (dev != DEVICE_MINOR) | ||
ret = -ENXIO; | ||
|
||
priv = container_of(inode->i_cdev, struct rpi_gpiomem_priv, | ||
rpi_gpiomem_cdev); | ||
if (!priv) | ||
return -EINVAL; | ||
file->private_data = priv; | ||
return ret; | ||
} | ||
|
||
static int rpi_gpiomem_release(struct inode *inode, struct file *file) | ||
{ | ||
int dev = iminor(inode); | ||
int ret = 0; | ||
|
||
if (dev != DEVICE_MINOR) | ||
ret = -ENXIO; | ||
|
||
return ret; | ||
} | ||
|
||
static const struct vm_operations_struct rpi_gpiomem_vm_ops = { | ||
#ifdef CONFIG_HAVE_IOREMAP_PROT | ||
.access = generic_access_phys | ||
#endif | ||
}; | ||
|
||
static int rpi_gpiomem_mmap(struct file *file, struct vm_area_struct *vma) | ||
{ | ||
int i; | ||
struct rpi_gpiomem_priv *priv; | ||
unsigned long base; | ||
unsigned long len = 0; | ||
unsigned long offset; | ||
|
||
priv = file->private_data; | ||
/* | ||
* Userspace must provide a virtual address space at least | ||
* the size of the concatenated ranges. | ||
*/ | ||
for (i = 0; i < priv->nr_wins; i++) | ||
len += priv->iowins[i].len; | ||
if (len > vma->vm_end - vma->vm_start + 1) | ||
return -EINVAL; | ||
|
||
vma->vm_ops = &rpi_gpiomem_vm_ops; | ||
offset = vma->vm_start; | ||
for (i = 0; i < priv->nr_wins; i++) { | ||
base = priv->iowins[i].phys_base >> PAGE_SHIFT; | ||
len = priv->iowins[i].len; | ||
vma->vm_page_prot = phys_mem_access_prot(file, base, len, | ||
vma->vm_page_prot); | ||
if (remap_pfn_range(vma, offset, | ||
base, len, | ||
vma->vm_page_prot)) | ||
break; | ||
offset += len; | ||
} | ||
|
||
if (i < priv->nr_wins) | ||
return -EAGAIN; | ||
|
||
return 0; | ||
} | ||
|
||
static const struct file_operations rpi_gpiomem_fops = { | ||
.owner = THIS_MODULE, | ||
.open = rpi_gpiomem_open, | ||
.release = rpi_gpiomem_release, | ||
.mmap = rpi_gpiomem_mmap, | ||
}; | ||
|
||
static const struct of_device_id rpi_gpiomem_of_match[]; | ||
|
||
static int rpi_gpiomem_probe(struct platform_device *pdev) | ||
{ | ||
int err, i; | ||
const struct of_device_id *id; | ||
struct device *dev = &pdev->dev; | ||
struct device_node *node = dev->of_node; | ||
struct resource *ioresource; | ||
struct rpi_gpiomem_priv *priv; | ||
|
||
/* Allocate buffers and instance data */ | ||
|
||
priv = kzalloc(sizeof(struct rpi_gpiomem_priv), GFP_KERNEL); | ||
|
||
if (!priv) { | ||
err = -ENOMEM; | ||
goto failed_inst_alloc; | ||
} | ||
platform_set_drvdata(pdev, priv); | ||
|
||
priv->dev = dev; | ||
id = of_match_device(rpi_gpiomem_of_match, dev); | ||
if (!id) | ||
return -EINVAL; | ||
|
||
/* | ||
* Device node naming - for legacy (bcm2835) DT bindings, the driver | ||
* created the node based on a hardcoded name - for new bindings, | ||
* take the node name from DT. | ||
*/ | ||
if (id == &rpi_gpiomem_of_match[0]) { | ||
priv->name = "gpiomem"; | ||
} else { | ||
err = of_property_read_string(node, "chardev-name", &priv->name); | ||
if (err) | ||
return -EINVAL; | ||
} | ||
|
||
/* | ||
* Go find the register ranges associated with this instance | ||
*/ | ||
for (i = 0; i < MAX_RANGES; i++) { | ||
ioresource = platform_get_resource(pdev, IORESOURCE_MEM, i); | ||
if (!ioresource && i == 0) { | ||
dev_err(priv->dev, "failed to get IO resource - no ranges available\n"); | ||
err = -ENOENT; | ||
goto failed_get_resource; | ||
} | ||
if (!ioresource) | ||
break; | ||
|
||
priv->iowins[i].phys_base = ioresource->start; | ||
priv->iowins[i].len = (ioresource->end + 1) - ioresource->start; | ||
dev_info(&pdev->dev, "window base 0x%08lx size 0x%08lx\n", | ||
priv->iowins[i].phys_base, priv->iowins[i].len); | ||
priv->nr_wins++; | ||
} | ||
|
||
/* Create character device entries */ | ||
|
||
err = alloc_chrdev_region(&priv->devid, | ||
DEVICE_MINOR, 1, priv->name); | ||
if (err != 0) { | ||
dev_err(priv->dev, "unable to allocate device number"); | ||
goto failed_alloc_chrdev; | ||
} | ||
cdev_init(&priv->rpi_gpiomem_cdev, &rpi_gpiomem_fops); | ||
priv->rpi_gpiomem_cdev.owner = THIS_MODULE; | ||
err = cdev_add(&priv->rpi_gpiomem_cdev, priv->devid, 1); | ||
if (err != 0) { | ||
dev_err(priv->dev, "unable to register device"); | ||
goto failed_cdev_add; | ||
} | ||
|
||
/* Create sysfs entries */ | ||
|
||
priv->class = class_create(priv->name); | ||
if (IS_ERR(priv->class)) { | ||
err = PTR_ERR(priv->class); | ||
goto failed_class_create; | ||
} | ||
|
||
dev = device_create(priv->class, NULL, priv->devid, NULL, priv->name); | ||
if (IS_ERR(dev)) { | ||
err = PTR_ERR(dev); | ||
goto failed_device_create; | ||
} | ||
|
||
dev_info(priv->dev, "initialised %u regions as /dev/%s\n", | ||
priv->nr_wins, priv->name); | ||
|
||
return 0; | ||
|
||
failed_device_create: | ||
class_destroy(priv->class); | ||
failed_class_create: | ||
cdev_del(&priv->rpi_gpiomem_cdev); | ||
failed_cdev_add: | ||
unregister_chrdev_region(priv->devid, 1); | ||
failed_alloc_chrdev: | ||
failed_get_resource: | ||
kfree(priv); | ||
failed_inst_alloc: | ||
dev_err(&pdev->dev, "could not load rpi_gpiomem"); | ||
return err; | ||
} | ||
|
||
static void rpi_gpiomem_remove(struct platform_device *pdev) | ||
{ | ||
struct device *dev = &pdev->dev; | ||
struct rpi_gpiomem_priv *priv = platform_get_drvdata(pdev); | ||
|
||
device_destroy(priv->class, priv->devid); | ||
class_destroy(priv->class); | ||
cdev_del(&priv->rpi_gpiomem_cdev); | ||
unregister_chrdev_region(priv->devid, 1); | ||
kfree(priv); | ||
|
||
dev_info(dev, "%s driver removed - OK", priv->name); | ||
} | ||
|
||
static const struct of_device_id rpi_gpiomem_of_match[] = { | ||
{ | ||
.compatible = "brcm,bcm2835-gpiomem", | ||
}, | ||
{ | ||
.compatible = "raspberrypi,gpiomem", | ||
}, | ||
{ /* sentinel */ }, | ||
}; | ||
|
||
MODULE_DEVICE_TABLE(of, rpi_gpiomem_of_match); | ||
|
||
static struct platform_driver rpi_gpiomem_driver = { | ||
.probe = rpi_gpiomem_probe, | ||
.remove = rpi_gpiomem_remove, | ||
.driver = { | ||
.name = DRIVER_NAME, | ||
.owner = THIS_MODULE, | ||
.of_match_table = rpi_gpiomem_of_match, | ||
}, | ||
}; | ||
|
||
module_platform_driver(rpi_gpiomem_driver); | ||
|
||
MODULE_ALIAS("platform:rpi-gpiomem"); | ||
MODULE_LICENSE("Dual BSD/GPL"); | ||
MODULE_DESCRIPTION("Driver for accessing GPIOs from userspace"); | ||
MODULE_AUTHOR("Jonathan Bell <jonathan@raspberrypi.com>"); |