[Linux] M1/S Kernel Driver: gpiomem

13 minute read

I realized I haven’t done a driver code review yet, so I’m starting one now.
The target is the M1S kernel released by Hardkernel late last year.
I’ll be looking at the simple gpiomem driver.

Overview

The gpiomem driver is a device driver that isolates only the GPIO region within memory.
When hardware is controlled from userspace, it is typically done with root privileges.

But can we control GPIO through the /dev/mem node?
It’s possible, but if you accidentally access a non-GPIO address and change register values…
Writes performed with root privileges are applied immediately.
While it might not always cause a major issue, it could significantly impact the system.

To prevent such situations, the node is restricted to open only GPIO addresses.
For instance, wiringPi uses /dev/gpiomem,
and gpiod uses /dev/gpiochip[n] nodes.

There may be other cases, but let’s start with the gpiomem driver.

Code Review

This review is based on kernel version 5.10.y.
Before reading the review, it might be helpful to read the document on device drivers.
It outlines the essential components that must be included.

gpiomem.c

The source code is rk3568-gpiomem.c.

Headers and preprocessor directives:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.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 DEVICE_NAME "rk3568-gpiomem"
#define DRIVER_NAME "gpiomem-rk3568"
#define DEVICE_MINOR 0

<linux/kernel.h> : Kernel API support
<linux/module.h> : Kernel module API/MACRO support
<linux/of.h> : Kernel match table support
<linux/platform_device.h> : Kernel platform device support
<linux/mm.h> : Kernel memory type support
<linux/slab.h> : Support for kernel memory operations like kzalloc and kfree
<linux/cdev.h> : Character device API support
<linux/pagemap.h> : Kernel page-related support
<linux/io.h> : Kernel I/O API support

Static global variable and structure declarations:

struct rk3568_gpiomem_instance {
	unsigned long gpio_regs_phys;
	struct device *dev;
};

static struct cdev rk3568_gpiomem_cdev;
static dev_t rk3568_gpiomem_devid;
static struct class *rk3568_gpiomem_class;
static struct device *rk3568_gpiomem_dev;
static struct rk3568_gpiomem_instance *inst;

struct rk3568_gpiomem_instance
Used for the driver’s probe and remove functions.
Contains the physical address of the registers and a device structure for hardware interaction.
Utilized as static struct rk3568_gpiomem_instance *inst.

static struct cdev rk3568_gpiomem_cdev
Represents the character device node.

static struct dev_t rk3568_gpiomem_devid
Used to specify the major and minor numbers of the device.

static struct class *rk3568_gpiomem_class
Creates a device node under sysfs.
Includes static struct device *rk3568_gpiomem_dev for creating that device node.

First, we must implement the file_operations for the driver.
This is because in UNIX systems, hardware is mostly treated as a file.

static int rk3568_gpiomem_open(struct inode *inode, struct file *file)
{
	int dev = iminor(inode);
	int ret = 0;

	dev_info(inst->dev, "gpiomem device opened.");

	if (dev != DEVICE_MINOR) {
		dev_err(inst->dev, "Unknown minor device: %d", dev);
		ret = -ENXIO;
	}
	return ret;
}

static int rk3568_gpiomem_release(struct inode *inode, struct file *file)
{
	int dev = iminor(inode);
	int ret = 0;

	if (dev != DEVICE_MINOR) {
		dev_err(inst->dev, "Unknown minor device %d", dev);
		ret = -ENXIO;
	}
	return ret;
}

static const struct vm_operations_struct rk3568_gpiomem_vm_ops = {
#ifdef CONFIG_HAVE_IOREMAP_PROT
	.access = generic_access_phys
#endif
};

static int address_is_allowed(unsigned long pfn, unsigned long size)
{
	unsigned long address = pfn << PAGE_SHIFT;
	
	dev_info(inst->dev, "address_is_allowed.pfn: 0x%08lx", address);
	
	switch(address) {
		case 0xfdd00000:
		case 0xfdd20000:
		case 0xfdc20000:
		case 0xfdd60000:
		case 0xfdd70000:
		case 0xfe6f0000:
		case 0xfe740000:
		case 0xfe750000:
		case 0xfe760000:
		case 0xfe770000:
		case 0xfdc60000:
			dev_info(inst->dev, "address_is_allowed.return 1");
			return 1;
			break;
		default :
			dev_info(inst->dev, "address_is_allowed.return 0");
				return 0;
    }
}

static int rk3568_gpiomem_mmap(struct file *file, struct vm_area_struct *vma)
{

	size_t size;

	size = vma->vm_end - vma->vm_start;


	if (!address_is_allowed(vma->vm_pgoff, size))
		return -EPERM;

	vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);

	vma->vm_ops =  &rk3568_gpiomem_vm_ops;

	/* Remap-pfn-range will mark the range VM_IO */
	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}

	return 0;
}

static const struct file_operations
rk3568_gpiomem_fops = {
	.owner = THIS_MODULE,
	.open = rk3568_gpiomem_open,
	.release = rk3568_gpiomem_release,
	.mmap = rk3568_gpiomem_mmap,
};

file_operations are required during device driver initialization.
The driver code defines the owner, open, release, and mmap attributes for file_operations.

Let’s follow the file_operations.

First, THIS_MODULE in the owner field is a module macro provided by the kernel.
Unless there is a specific reason, it is typically used as the default value.


open function:

static int rk3568_gpiomem_open(struct inode *inode, struct file *file)
{
	int dev = iminor(inode);
	int ret = 0;

	dev_info(inst->dev, "gpiomem device opened.");

	if (dev != DEVICE_MINOR) {
		dev_err(inst->dev, "Unknown minor device: %d", dev);
		ret = -ENXIO;
	}
	return ret;
}

The iminor function retrieves the minor number of the device node.
If it differs from the driver’s minor number, dev_err is called.
The error log ( ENXIO ) corresponds to “No such device or address”.


release function:

static int rk3568_gpiomem_release(struct inode *inode, struct file *file)
{
	int dev = iminor(inode);
	int ret = 0;

	if (dev != DEVICE_MINOR) {
		dev_err(inst->dev, "Unknown minor device %d", dev);
		ret = -ENXIO;
	}
	return ret;
}

It’s not much different from the open function.
Neither performs any hardware operations; their purpose is simply to log messages (i.e., indicating when open/release occurs).


mmap function:

static int rk3568_gpiomem_mmap(struct file *file, struct vm_area_struct *vma)
{

	size_t size;

	size = vma->vm_end - vma->vm_start;


	if (!address_is_allowed(vma->vm_pgoff, size))
		return -EPERM;

	vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);

	vma->vm_ops =  &rk3568_gpiomem_vm_ops;

	/* Remap-pfn-range will mark the range VM_IO */
	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}

	return 0;
}

Among the function parameters is vm_area_struct.
This is a virtual memory structure, but first, we need to understand what mmap is.

mmap stands for memory mapping.
The kernel performs memory mapping by creating separate virtual addresses,
and these virtual address regions are defined using vm_area_struct.
Understanding this requires knowledge of virtual memory and paging techniques.
Since memory is not our primary topic, let’s move on with the code review.

The size of the VMA is vma->vm_end - vma->vm_start.
These represent the end and start addresses of the VMA, respectively.
Naturally, subtracting the start address from the end address gives the size of the region.

And there is a custom function in the middle:

	if (!address_is_allowed(vma->vm_pgoff, size))
		return -EPERM;

As mentioned, gpiomem isolates only the GPIO region.
This step verifies the region.
It checks if the vma->vm_pgoff value is within the GPIO region.
vma->vm_pgoff represents the page offset of the VMA.
This is the starting position of the actual file.
A virtual memory region contains multiple pages, typically 4K to 8K in size.
If you’re familiar with paging, you’ll know that the actual file start is not the same as the virtual memory start address.
This is called the Page Frame Number, or PFN for short.


This section checks whether the address pointed to by the file descriptor is indeed within the GPIO region when using gpiomem.

static int address_is_allowed(unsigned long pfn, unsigned long size)
{
	unsigned long address = pfn << PAGE_SHIFT;
	
	dev_info(inst->dev, "address_is_allowed.pfn: 0x%08lx", address);
	
	switch(address) {
		case 0xfdd00000:
		case 0xfdd20000:
		case 0xfdc20000:
		case 0xfdd60000:
		case 0xfdd70000:
		case 0xfe6f0000:
		case 0xfe740000:
		case 0xfe750000:
		case 0xfe760000:
		case 0xfe770000:
		case 0xfdc60000:
			dev_info(inst->dev, "address_is_allowed.return 1");
			return 1;
			break;
		default :
			dev_info(inst->dev, "address_is_allowed.return 0");
				return 0;
    }
}

It actually receives the pfn as a parameter.
address is the physical address.
Generally, the operation pfn « PAGE_SHIFT converts it to a physical address,
conversely, phy » PAGE_SHIFT yields the pfn value.
PAGE_SHIFT typically has a value between 12 and 16.
This value varies depending on the architecture.

The converted address is then compared with the GPIO region addresses.
A switch-case statement determines whether to proceed with mmap or terminate based on the return value.
The GPIO region can be identified by checking the chip’s datasheet.

gpiomem-datasheet


There are a few more addresses in the actual function, but I’ll skip them as they’re not relevant to this post.
In any case, you can see that all relevant addresses are included.


Let’s return to the mmap code.

static int rk3568_gpiomem_mmap(struct file *file, struct vm_area_struct *vma)
{

	size_t size;

	size = vma->vm_end - vma->vm_start;


	if (!address_is_allowed(vma->vm_pgoff, size))
		return -EPERM;

	vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);

	vma->vm_ops =  &rk3568_gpiomem_vm_ops;

	/* Remap-pfn-range will mark the range VM_IO */
	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}

	return 0;
}

vma->vm_page_prot represents the permissions (flags) for the virtual memory region.

prot is short for “protection”.
Just like pgoff, understanding the abbreviations used for kernel variables makes the code much easier to read.
Anyway, memory access permissions are obtained via the phys_mem_access_prot function.

Virtual memory VMA, like a file, must also have its operations defined
defined by the following statement: vma->vm_ops = &rk3568_gpiomem_vm_ops;.


These operations are defined within the code.

static const struct vm_operations_struct rk3568_gpiomem_vm_ops = {
#ifdef CONFIG_HAVE_IOREMAP_PROT
	.access = generic_access_phys
#endif
};


Finally, the file must be mapped to the virtual memory region.
As mentioned previously, I will not be explaining the virtual memory structure in detail.

	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}

This is the most critical part of the mmap function.
The remap_pfn_range function is used for memory mapping.
It creates the page table for the virtual address.
Ultimately, it establishes a mapping between vma->pgoff (the virtual address) and the actual hardware physical address.
The mapping occurs within the VMA region (vm_start to size).
That’s the basic idea.
The error log ( EAGAIN ) corresponds to “No more processes”.


Once file_operations are defined, we need to define the device driver.
Driver definition section:

static int rk3568_gpiomem_probe(struct platform_device *pdev)
{
	int err;
	void *ptr_err;
	struct device *dev = &pdev->dev;
	struct resource *ioresource;

	/* Allocate buffers and instance data */

	inst = kzalloc(sizeof(struct rk3568_gpiomem_instance), GFP_KERNEL);

	if (!inst) {
		err = -ENOMEM;
		goto failed_inst_alloc;
	}

	inst->dev = dev;

	ioresource = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (ioresource) {
		inst->gpio_regs_phys = ioresource->start;
	} else {
		dev_err(inst->dev, "failed to get IO resource");
		err = -ENOENT;
		goto failed_get_resource;
	}

	/* Create character device entries */

	err = alloc_chrdev_region(&rk3568_gpiomem_devid,
				  DEVICE_MINOR, 1, DEVICE_NAME);
	if (err != 0) {
		dev_err(inst->dev, "unable to allocate device number");
		goto failed_alloc_chrdev;
	}
	cdev_init(&rk3568_gpiomem_cdev, &rk3568_gpiomem_fops);
	rk3568_gpiomem_cdev.owner = THIS_MODULE;
	err = cdev_add(&rk3568_gpiomem_cdev, rk3568_gpiomem_devid, 1);
	if (err != 0) {
		dev_err(inst->dev, "unable to register device");
		goto failed_cdev_add;
	}

	/* Create sysfs entries */

	rk3568_gpiomem_class = class_create(THIS_MODULE, DEVICE_NAME);
	ptr_err = rk3568_gpiomem_class;
	if (IS_ERR(ptr_err))
		goto failed_class_create;

	rk3568_gpiomem_dev = device_create(rk3568_gpiomem_class, NULL,
					rk3568_gpiomem_devid, NULL,
					"gpiomem");
	ptr_err = rk3568_gpiomem_dev;
	if (IS_ERR(ptr_err))
		goto failed_device_create;

	dev_info(inst->dev, "Initialised: Registers at 0x%08lx",
		inst->gpio_regs_phys);

	return 0;

failed_device_create:
	class_destroy(rk3568_gpiomem_class);
failed_class_create:
	cdev_del(&rk3568_gpiomem_cdev);
	err = PTR_ERR(ptr_err);
failed_cdev_add:
	unregister_chrdev_region(rk3568_gpiomem_devid, 1);
failed_alloc_chrdev:
failed_get_resource:
	kfree(inst);
failed_inst_alloc:
	dev_err(inst->dev, "could not load rk3568_gpiomem");
	return err;
}

static int rk3568_gpiomem_remove(struct platform_device *pdev)
{
	struct device *dev = inst->dev;

	kfree(inst);
	device_destroy(rk3568_gpiomem_class, rk3568_gpiomem_devid);
	class_destroy(rk3568_gpiomem_class);
	cdev_del(&rk3568_gpiomem_cdev);
	unregister_chrdev_region(rk3568_gpiomem_devid, 1);

	dev_info(dev, "GPIO mem driver removed - OK");
	return 0;
}

static const struct of_device_id rk3568_gpiomem_of_match[] = {
	{.compatible = "rockchip,rk3568-gpiomem",},
	{ /* sentinel */ },
};

MODULE_DEVICE_TABLE(of, rk3568_gpiomem_of_match);

static struct platform_driver rk3568_gpiomem_driver = {
	.probe = rk3568_gpiomem_probe,
	.remove = rk3568_gpiomem_remove,
	.driver = {
		   .name = DRIVER_NAME,
		   .owner = THIS_MODULE,
		   .of_match_table = rk3568_gpiomem_of_match,
		   },
};

module_platform_driver(rk3568_gpiomem_driver);

MODULE_ALIAS("platform:gpiomem-rk3568");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("gpiomem driver for accessing GPIO from userspace");
MODULE_AUTHOR("Luke Wren <luke@raspberrypi.org>");


Like file_operations, let’s examine the core parts one by one.

static struct platform_driver rk3568_gpiomem_driver = {
	.probe = rk3568_gpiomem_probe,
	.remove = rk3568_gpiomem_remove,
	.driver = {
		   .name = DRIVER_NAME,
		   .owner = THIS_MODULE,
		   .of_match_table = rk3568_gpiomem_of_match,
		   },
};

It includes probe and remove for driver registration/unregistration, along with driver components like name, owner, and of_match_table.

The driver’s name and owner fields use strings and macros.


probe section:

static int rk3568_gpiomem_probe(struct platform_device *pdev)
{
	int err;
	void *ptr_err;
	struct device *dev = &pdev->dev;
	struct resource *ioresource;

	/* Allocate buffers and instance data */

	inst = kzalloc(sizeof(struct rk3568_gpiomem_instance), GFP_KERNEL);

	if (!inst) {
		err = -ENOMEM;
		goto failed_inst_alloc;
	}

	inst->dev = dev;

	ioresource = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (ioresource) {
		inst->gpio_regs_phys = ioresource->start;
	} else {
		dev_err(inst->dev, "failed to get IO resource");
		err = -ENOENT;
		goto failed_get_resource;
	}

	/* Create character device entries */

	err = alloc_chrdev_region(&rk3568_gpiomem_devid,
				  DEVICE_MINOR, 1, DEVICE_NAME);
	if (err != 0) {
		dev_err(inst->dev, "unable to allocate device number");
		goto failed_alloc_chrdev;
	}
	cdev_init(&rk3568_gpiomem_cdev, &rk3568_gpiomem_fops);
	rk3568_gpiomem_cdev.owner = THIS_MODULE;
	err = cdev_add(&rk3568_gpiomem_cdev, rk3568_gpiomem_devid, 1);
	if (err != 0) {
		dev_err(inst->dev, "unable to register device");
		goto failed_cdev_add;
	}

	/* Create sysfs entries */

	rk3568_gpiomem_class = class_create(THIS_MODULE, DEVICE_NAME);
	ptr_err = rk3568_gpiomem_class;
	if (IS_ERR(ptr_err))
		goto failed_class_create;

	rk3568_gpiomem_dev = device_create(rk3568_gpiomem_class, NULL,
					rk3568_gpiomem_devid, NULL,
					"gpiomem");
	ptr_err = rk3568_gpiomem_dev;
	if (IS_ERR(ptr_err))
		goto failed_device_create;

	dev_info(inst->dev, "Initialised: Registers at 0x%08lx",
		inst->gpio_regs_phys);

	return 0;

failed_device_create:
	class_destroy(rk3568_gpiomem_class);
failed_class_create:
	cdev_del(&rk3568_gpiomem_cdev);
	err = PTR_ERR(ptr_err);
failed_cdev_add:
	unregister_chrdev_region(rk3568_gpiomem_devid, 1);
failed_alloc_chrdev:
failed_get_resource:
	kfree(inst);
failed_inst_alloc:
	dev_err(inst->dev, "could not load rk3568_gpiomem");
	return err;
}

First, the line inst = kzalloc(sizeof(struct rk3568_gpiomem_instance), GFP_KERNEL);.
The kzalloc function is similar to malloc.
Think of it as the kernel version of dynamic memory allocation; kzalloc also initializes the memory to zero.
GFP_KERNEL is a flag indicating that memory should only be allocated if it is available.

The statement inst->dev = dev; connects the dev of the instance defined at the beginning of the code to the dev of pdev.


This section passes hardware resources to the instance.

	ioresource = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (ioresource) {
		inst->gpio_regs_phys = ioresource->start;
	} else {
		dev_err(inst->dev, "failed to get IO resource");
		err = -ENOENT;
		goto failed_get_resource;
	}

Maps the register physical address to the resource start address.
The error log ( ENOENT ) corresponds to “No such file or directory”.


Next is the device driver initialization.
Since it’s a char device driver, cdev_init is used for initialization.
Major/minor numbers are also allocated using the alloc_chrdev_region function.

	/* Create character device entries */

	err = alloc_chrdev_region(&rk3568_gpiomem_devid,
				  DEVICE_MINOR, 1, DEVICE_NAME);
	if (err != 0) {
		dev_err(inst->dev, "unable to allocate device number");
		goto failed_alloc_chrdev;
	}
	cdev_init(&rk3568_gpiomem_cdev, &rk3568_gpiomem_fops);
	rk3568_gpiomem_cdev.owner = THIS_MODULE;
	err = cdev_add(&rk3568_gpiomem_cdev, rk3568_gpiomem_devid, 1);
	if (err != 0) {
		dev_err(inst->dev, "unable to register device");
		goto failed_cdev_add;
	}

The cdev_init function takes the target cdev and its ops as parameters.
It uses the file_operations defined earlier.
goto is used for error handling.
I won’t cover that in detail here.


Class device section:
Connects the device under /sys/class/.
The major/minor numbers allocated above serve as the interface.

	/* Create sysfs entries */

	rk3568_gpiomem_class = class_create(THIS_MODULE, DEVICE_NAME);
	ptr_err = rk3568_gpiomem_class;
	if (IS_ERR(ptr_err))
		goto failed_class_create;

	rk3568_gpiomem_dev = device_create(rk3568_gpiomem_class, NULL,
					rk3568_gpiomem_devid, NULL,
					"gpiomem");
	ptr_err = rk3568_gpiomem_dev;
	if (IS_ERR(ptr_err))
		goto failed_device_create;

	dev_info(inst->dev, "Initialised: Registers at 0x%08lx",
		inst->gpio_regs_phys);

	return 0;

The device name and MAJOR/MINOR numbers can be identified through the uevent node.
goto is used for error handling.
Again, I won’t go into detail here.


remove section:

static int rk3568_gpiomem_remove(struct platform_device *pdev)
{
	struct device *dev = inst->dev;

	kfree(inst);
	device_destroy(rk3568_gpiomem_class, rk3568_gpiomem_devid);
	class_destroy(rk3568_gpiomem_class);
	cdev_del(&rk3568_gpiomem_cdev);
	unregister_chrdev_region(rk3568_gpiomem_devid, 1);

	dev_info(dev, "GPIO mem driver removed - OK");
	return 0;
}

Removes the allocated memory, device, class, and character device,
and unregisters the device’s major and minor numbers.

We’re almost there.


Back to the driver registration section.

static struct platform_driver rk3568_gpiomem_driver = {
	.probe = rk3568_gpiomem_probe,
	.remove = rk3568_gpiomem_remove,
	.driver = {
		   .name = DRIVER_NAME,
		   .owner = THIS_MODULE,
		   .of_match_table = rk3568_gpiomem_of_match,
		   },
};

module_platform_driver(rk3568_gpiomem_driver);

The remaining part is of_match_table.
The match table is provided by the driver to allow the device-tree to manage the device.
It’s a table provided by the driver.
It uses the compatible attribute from the tree.
The driver is loaded by comparing the compatible value in the tree with the values in the driver’s match table.
The value is a string.

As for why it’s managed this way,
it’s a bit off-topic, but in kernel version 2.*.y, the register function was called every time a driver was added.
the register function was called.
However, as the number and variety of devices grew, management became increasingly difficult.
Consequently, when the Linux kernel major version moved to 3, the device-tree was introduced.
With device drivers now managed via the compatible value in the tree, the concept of a match table was also established.


In any case, we define the match table this way and register it using a macro.

static const struct of_device_id rk3568_gpiomem_of_match[] = {
	{.compatible = "rockchip,rk3568-gpiomem",},
	{ /* sentinel */ },
};

MODULE_DEVICE_TABLE(of, rk3568_gpiomem_of_match);

Registers the mapping table using the MODULE_DEVICE_TABLE macro,
which requires <linux/of.h>.


The code is now complete.
The final section of the code includes the driver module alias, license, description, and author.

MODULE_ALIAS("platform:gpiomem-rk3568");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("gpiomem driver for accessing GPIO from userspace");
MODULE_AUTHOR("Luke Wren <luke@raspberrypi.org>");

That’s all.

Leave a comment