How to make an embedded Linux device driver (5)

5th: Implementation of GPIO Devadora for Raspberry Pi

About this series

This is a HowTo article for developing embedded Linux device drivers as kernel modules. All the content of this article can be run on a Raspberry Pi.

-1st time: Build environment preparation and simple kernel module creation -Second time: System call handler and driver registration (static method) -Third time: System call handler and driver registration (dynamic method) -4th: Read / write implementation and memory story --__ 5th time: Implementation of GPIO driver for Raspberry Pi <------------------ --- Contents of this time __ -6th time: Implementation of ioctl -7th time: Interface for procfs -8th time: Interface for debugfs -9th time: Call a function of another kernel module / Use GPIO control function -10th time: Create a device driver using I2C -11th time: Add I2C device to device tree -12th time: Load the created device driver at startup

The entire source code that appears in this article

https://github.com/take-iwiw/DeviceDriverLesson/tree/master/05_01

Contents of this time

Up to the last time, we have learned how to implement system calls (open, close, read, write) that perform basic operations with the device driver. This time, we will implement the GPIO device driver for Raspberry Pi using the contents so far.

Since the purpose is not to make a solid GPIO driver, for the sake of simplicity, we will only target GPIO4 and always use output mode. Output High / Low with write. Read returns the current output level as a string of "1" / "0".

Prerequisite knowledge

Memory story (again)

Last time, I explained the memory of Raspberry Pi. In Raspberry Pi 2, the physical address of the peripheral register seen from ARM (CPU) is 0x3F000000. This seems to have been 0x20000000 for the original Raspberry Pi. In fact, looking at the memory map of the BCM2835 Datasheet, I / O Peripherals says 0x20000000. It has been. As introduced last time, you can get this address with bcm_host_get_peripheral_address. However, for the sake of simplicity, we will fix it at 0x3F000000 this time. If Raspberry Pi 4 or 5 comes out in the future, this address may change.

Raspberry Pi hardware register access

As mentioned above, we know that the physical address of the peripheral register starts at 0x3F000000. You can also find out which register to hit specifically for GPIO control in BCM2835 Datasheet. It is listed. At this time, there is one caveat in how to read this data sheet. This data sheet contains a description of each register, as well as an address for each. However, that address will be a bus address starting with 0x7E000000. What we need is the physical address as seen by the CPU. We already know that the start address is 0x3F000000, so be sure to look only at the offset. For example, GPFSEL0 (GPIO Function Select 0) is 0x7E200000 in the data sheet, but the physical address seen from the CPU is 0x3F200000 (in the case of Raspberry Pi 2).

By the way, the registers used this time are as follows.

Trial implementation with user space program

I've made kernel modules so far, but in fact, register access is possible from user space. This time I just hit the GPIO control register, so I will implement it with a program on the user space for the time being. If this works, we will start implementing the device driver on the kernel module side later. Trying it in user space in advance has the advantage of being easier to debug.

To access a physical address from a userspace program, first open / dev / mem. Then use that file descriptor (fd) to mmap. I think it's okay to use only the size, but it seems that there are many cases where the page size (4KByte) is secured. In addition, ʻO_SYNC` is specified when opening. This invalidates the cache and provides immediate register access. (In the first place, I think that the cache is not used because it is a register, but just in case)

The code looks like this: You can access the register (0x3F000000) by reading and writing to the virtual address acquired by mmap. The explanation of the value set in each register is omitted. Please check the data sheet. Basically, it only sets and inputs / outputs values to GPIO4.

userGpio.c


#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>

/*Physical address of peripheral register(From the BCM2835 specifications) */
#define REG_ADDR_BASE        (0x3F000000)		/* bcm_host_get_peripheral_address()Is better*/
#define REG_ADDR_GPIO_BASE   (REG_ADDR_BASE + 0x00200000)
#define REG_ADDR_GPIO_LENGTH 4096
#define REG_ADDR_GPIO_GPFSEL_0     0x0000
#define REG_ADDR_GPIO_OUTPUT_SET_0 0x001C
#define REG_ADDR_GPIO_OUTPUT_CLR_0 0x0028
#define REG_ADDR_GPIO_LEVEL_0      0x0034

#define REG(addr) (*((volatile unsigned int*)(addr)))
#define DUMP_REG(addr) printf("%08X\n", REG(addr));

int main()
{
	int address;	/*Virtual address to GPIO register(User space) */
	int fd;

	/*Open device file for memory access*/
	if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
		perror("open");
		return -1;
	}

	/* ARM(CPU)Physical address seen from → Mapping to virtual address*/
	address = (int)mmap(NULL, REG_ADDR_GPIO_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, REG_ADDR_GPIO_BASE);
	if (address == MAP_FAILED) {
		perror("mmap");
		close(fd);
		return -1;
	}

	/*Set GPIO4 to output*/
	REG(address + REG_ADDR_GPIO_GPFSEL_0) = 1 << 12;

	/*High output of GPIO4*/
	REG(address + REG_ADDR_GPIO_OUTPUT_SET_0) = 1 << 4;
	DUMP_REG(address + REG_ADDR_GPIO_LEVEL_0);

	/*Low output of GPIO4*/
	REG(address + REG_ADDR_GPIO_OUTPUT_CLR_0) = 1 << 4;
	DUMP_REG(address + REG_ADDR_GPIO_LEVEL_0);

	/*Release used resources*/
	munmap((void*)address, REG_ADDR_GPIO_LENGTH);
	close(fd);

	return 0;
}

Try to move

Connect the GPIO4 pin (the pin to the right of the SCL) to 3.3V via the LED and resistor. Build and execute with the following command.

gcc userGpio.c
sudo ./a.out

As you can see from the code, the LED lights up because GPIO4 is output low at the end. If you comment out this line, GPIO4 will be output High and the LED will turn off.

Implement the device driver as a kernel module

specification

Finally, make a real device driver. As mentioned at the beginning, this time we will use a very simple specification for simplicity.

Implementation

In kernel space, use ʻioremap_nocache` to translate physical addresses to virtual addresses (non-cached). Regarding this address translation, I wanted to translate a large area (for example, 4KByte) at once when loading or opening the kernel module, but the kernel space has only one virtual address space as a whole. So, I wondered if only one module could occupy so much, so I decided to convert 4 bytes each time I accessed the register. Of course, it is disadvantageous in terms of speed. What do you usually do? If you have any knowledge, please let me know. (In the case of kernel space, I feel that virtual addresses are not assigned, but just translated from physical addresses, so it may not be a waste of address space.)

After address translation, register access can be performed in the same way as a user space program. The code looks like this: The processing in mydevice_open, mydevice_read, mydevice_write is important. Other than that, init and exit are the same as last time. I thought I'd omit it, but since it's a separate article, I'll post it all.

Regarding the exchange of data with the user space, I used copy_to_user and copy_from_user last time. Since the data to be exchanged is only 1Byte this time, I tried using put_user and get_user.

myDeviceDriver.c


#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/*Physical address of peripheral register(From the BCM2835 specifications) */
#define REG_ADDR_BASE        (0x3F000000)		/* bcm_host_get_peripheral_address()Is better*/
#define REG_ADDR_GPIO_BASE   (REG_ADDR_BASE + 0x00200000)
#define REG_ADDR_GPIO_GPFSEL_0     0x0000
#define REG_ADDR_GPIO_OUTPUT_SET_0 0x001C
#define REG_ADDR_GPIO_OUTPUT_CLR_0 0x0028
#define REG_ADDR_GPIO_LEVEL_0      0x0034

#define REG(addr) (*((volatile unsigned int*)(addr)))
#define DUMP_REG(addr) printk("%08X\n", REG(addr));

/***Information about this device***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice"				/* /proc/Device name displayed on devices etc.*/
static const unsigned int MINOR_BASE = 0;	/*Starting number and number of minor numbers used in this device driver(=Number of devices) */
static const unsigned int MINOR_NUM  = 1;	/*Minor number is 0 only*/
static unsigned int mydevice_major;			/*Major number of this device driver(Dynamically decide) */
static struct cdev mydevice_cdev;			/*Character device object*/
static struct class *mydevice_class = NULL;	/*Device driver class object*/

/*Function called at open*/
static int mydevice_open(struct inode *inode, struct file *file)
{
	printk("mydevice_open");

	/* ARM(CPU)Physical address seen from → Virtual address(Kernel space)Mapping to*/
	int address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_GPFSEL_0, 4);

	/*Set GPIO4 to output*/
	REG(address) = 1 << 12;

	iounmap((void*)address);

	return 0;
}

/*Function called at close*/
static int mydevice_close(struct inode *inode, struct file *file)
{
	printk("mydevice_close");
	return 0;
}

/*Function called when reading*/
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	printk("mydevice_read");

	/* ARM(CPU)Physical address seen from → Virtual address(Kernel space)Mapping to*/
	int address = address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_LEVEL_0, 4);
	int val = (REG(address) & (1 << 4)) != 0;	/*0 if GPIO4 is 0,Set to 1*/

	/*Returns the GPIO output value to the user as a character*/
	put_user(val + '0', &buf[0]);

	iounmap((void*)address);

	return count;
}

/*Function called when writing*/
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	printk("mydevice_write");

	int address;
	char outValue;

	/*Get the output value to GPIO set by the user*/
	get_user(outValue, &buf[0]);

	/* ARM(CPU)Physical address seen from → Virtual address(Kernel space)Mapping to*/
	if(outValue == '1') {
		/* '1'Then SET*/
		address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_OUTPUT_SET_0, 4);
	} else {
		/* '0'Then CLR*/
		address = (int)ioremap_nocache(REG_ADDR_GPIO_BASE + REG_ADDR_GPIO_OUTPUT_CLR_0, 4);
	}
	REG(address) = 1 << 4;
	iounmap((void*)address);
	
	return count;
}

/*Handler table for various system calls*/
struct file_operations s_mydevice_fops = {
	.open    = mydevice_open,
	.release = mydevice_close,
	.read    = mydevice_read,
	.write   = mydevice_write,
};

/*Road(insmod)Functions sometimes called*/
static int mydevice_init(void)
{
	printk("mydevice_init\n");

	int alloc_ret = 0;
	int cdev_err = 0;
	dev_t dev;

	/* 1.Secure a free major number*/
	alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
	if (alloc_ret != 0) {
		printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
		return -1;
	}

	/* 2.Obtained dev( =Major number+Minor number)Get the major number from and keep it*/
	mydevice_major = MAJOR(dev);
	dev = MKDEV(mydevice_major, MINOR_BASE);	/*Unnecessary? */

	/* 3.Initialization of cdev structure and registration of system call handler table*/
	cdev_init(&mydevice_cdev, &s_mydevice_fops);
	mydevice_cdev.owner = THIS_MODULE;

	/* 4.This device driver(cdev)In the kernel*/
	cdev_err = cdev_add(&mydevice_cdev, dev, MINOR_NUM);
	if (cdev_err != 0) {
		printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
		unregister_chrdev_region(dev, MINOR_NUM);
		return -1;
	}

	/* 5.Register the class of this device(/sys/class/mydevice/make) */
	mydevice_class = class_create(THIS_MODULE, "mydevice");
	if (IS_ERR(mydevice_class)) {
		printk(KERN_ERR  "class_create\n");
		cdev_del(&mydevice_cdev);
		unregister_chrdev_region(dev, MINOR_NUM);
		return -1;
	}

	/* 6. /sys/class/mydevice/mydevice*make*/
	for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
		device_create(mydevice_class, NULL, MKDEV(mydevice_major, minor), NULL, "mydevice%d", minor);
	}

	return 0;
}

/*Unload(rmmod)Functions sometimes called*/
static void mydevice_exit(void)
{
	printk("mydevice_exit\n");

	dev_t dev = MKDEV(mydevice_major, MINOR_BASE);
	
	/* 7. /sys/class/mydevice/mydevice*To delete*/
	for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
		device_destroy(mydevice_class, MKDEV(mydevice_major, minor));
	}

	/* 8.Remove class registration for this device(/sys/class/mydevice/To delete) */
	class_destroy(mydevice_class);

	/* 9.This device driver(cdev)From the kernel*/
	cdev_del(&mydevice_cdev);

	/* 10.Remove the major number registration used in this device driver*/
	unregister_chrdev_region(dev, MINOR_NUM);
}

module_init(mydevice_init);
module_exit(mydevice_exit);

Operation check

Build and embed it in the kernel as follows:

make
sudo insmod MyDeviceModule.ko
echo "0" > /dev/mydevice0
echo "1" > /dev/mydevice0
cat /dev/mydevice0
1111111111111111^C

Writing "0" or "1" with echo should change the output of GPIO4 and the LED should flicker. Also, reading with cat will display the output level at that time. However, it always returns a value, so you need to stop it with Ctrl-c.

bonus

It seems that you can check the physical memory map with the following command. If you look at this, you can see that it has the same content as we have seen so far. SDRAM is located from physical address 0. In the memory map below, it is 00000000-3b3fffff. 0x3b3fffff = 994050047 = About 1GByte, so it seems that we are actually meeting. The register address of Peri is also located at 0x3fXXXXXX.

sudo cat /proc/iomem
00000000-3b3fffff : System RAM
  00008000-00afffff : Kernel code
  00c00000-00d3da63 : Kernel data
3f006000-3f006fff : dwc_otg
3f007000-3f007eff : /soc/dma@7e007000
3f00b840-3f00b84e : /soc/vchiq
3f00b880-3f00b8bf : /soc/mailbox@7e00b880
3f101000-3f102fff : /soc/cprman@7e101000
3f200000-3f2000b3 : /soc/gpio@7e200000
3f201000-3f201fff : /soc/serial@7e201000
  3f201000-3f201fff : /soc/serial@7e201000
3f202000-3f2020ff : /soc/sdhost@7e202000
3f215000-3f215007 : /soc/aux@0x7e215000
3f980000-3f98ffff : dwc_otg

Recommended Posts

How to make an embedded Linux device driver (11)
How to make an embedded Linux device driver (8)
How to make an embedded Linux device driver (1)
How to make an embedded Linux device driver (4)
How to make an embedded Linux device driver (7)
How to make an embedded Linux device driver (2)
How to make an embedded Linux device driver (3)
How to make an embedded Linux device driver (6)
How to make an embedded Linux device driver (5)
How to make an embedded Linux device driver (10)
How to make an embedded Linux device driver (9)
How to make an embedded Linux device driver (12) (Complete)
[Blender x Python] How to make an animation
How to make Linux compatible with Japanese keyboard
How to make Yubico Yubikey recognized in Manjaro Linux
How to make an interactive CLI tool in Golang
[Python] How to make an adjacency matrix / adjacency list [Graph theory]
How to make a hacking lab-Kali Linux (2020.1) VirtualBox 64-bit Part 2-
How to make a Python package (written for an intern)
How to create an ISO file (CD image) on Linux
How to make a slack bot
How to install wkhtmltopdf (Amazon Linux2)
How to create an email user
How to install VMware-Tools on Linux
How to make a crawler --Advanced
How to make a recursive function
How to install MBDyn (Linux Ubuntu)
How to make a deadman's switch
[Blender] How to make a Blender plugin
[Blender] How to make Blender scripts multilingual
How to make a crawler --Basic
How to build MongoDB C driver
How to check Linux OS version
How to make a string into an array or an array into a string in Python
How to get the printer driver for Oki Mac into Linux
How to make Word Cloud characters monochromatic
How to build my own Linux server
How to make Selenium as light as possible
[Linux] How to subdivide files and folders
How to make an artificial intelligence LINE bot with Flask + LINE Messaging API
How to install aws-session-manager-plugin on Manajro Linux
[Python] How to make a class iterable
python3 How to install an external module
How to create an NVIDIA Docker environment
How to convert Python to an exe file
I want to know how LINUX works!
[Linux] How to use the echo command
How to update php on Amazon linux 2
How to display emoji on Manjaro Linux
[Cocos2d-x] How to make Script Binding (Part 2)
How to install Anisble on Amazon Linux 2
How to operate Linux from the console
How to install Windows Subsystem For Linux
How to power off Linux with Ultra96-V2
How to update security on CentOS Linux 8
How to make multi-boot USB (Windows 10 compatible)
How to make a Backtrader custom indicator
How to make a Pelican site map
[Cocos2d-x] How to make Script Binding (Part 1)
How to find large files on Linux
How to get started with laravel (Linux)