Character device driver Foundation (II)

Posted by stormchaser1_1 on Tue, 08 Feb 2022 17:25:20 +0100

Working principle of the whole system:
  1. Application layer - > API - > device driver - > Hardware
    1. Sending appropriate instructions can perform different operations. This rule is specified by the device driver.
    2. These rules are displayed through the api and called by the application layer
    3. The application layer transmits the corresponding parameters, and the device driver performs the corresponding operation through the parameter control hardware
  2. API: open, read, write, close system call
    1. open a device, read reads data from the device, and write writes data to the device
  3. The driver source code should provide real open, read, write, close and other function entities

Note: if we do not use unregister_ The chrdev function logs off the module. When we uninstall the module, we can also check the existence of the module by using cat /proc/devices

file_operation structure

  1. The element is mainly a function pointer, which is used to attach the address of the entity function
  2. Each device driver needs a variable of structure type and defines an opration to maintain read, write and close
  3. When the device driver registers with the kernel, it provides variables of this structure type. The kernel needs to know that you have this driver to register

Master number automatic assignment

The kernel knows which main device numbers it can use and actively distributes them to us. In this case, we don't need to check with cat, and then allocate them by ourselves

register_ Need to register with Linux kernel h>

  1. Function: Driver registers file with kernel_ Operation structure, function pointer and function entity are linked
  2. Parameters:
    1. major: main equipment number. This device number is similar to each class of equipment ID number, specifically distinguish the need secondary equipment number, the current equipment binding number can be specified by itself, or the kernel can be allocated. The parameter 0 is passed in, which means that the kernel will automatically allocate an appropriate blank unused master device number for us. If the kernel returns successfully, it will return the allocated master device number
    2. Name, the name of the device driver
    3. fops: structure pointer, pointing to the file we filled_ Operation, pass our structure variables to the function and complete the registration inside the function
  3. inline and static
    1. static to prevent conflicts with function names in other files
    2. inline can be expanded directly without worrying about repeated definitions when multiple files are called, and the function is very short, which can save overhead
How the kernel manages character device drivers:
  1. There is an array in the kernel to store the registered characters and device drivers. The subscript of the array is related to the main device number
  2. register_chrdev stores the information of the driver we want to register (mainly) in the corresponding position in the array
  3. cat /proc/devices view the character device drivers (and block device drivers) that have been registered in the kernel
Creation of drive device file:

(1) What is a device file: used to index this file

(2) The key information of the device file is the primary and secondary device number. When we open the device file through the upper api, we need to correspond to the corresponding file_operation structure, which corresponds to the corresponding array subscript, that is, the main device number

Create a device file using mknod: mknod /dev/xxx c primary device number secondary device number

Application driven data exchange api
  1. copy_from_user is used to copy data from user space to kernel space
  2. copry_to_user is used to copy data from kernel space to user space

The above two interfaces are somewhat different from the conventional copy function. The return value returns 0 if the copy is successful, and returns the number of bytes that have not been successfully copied if the copy is unsuccessful

How to operate the hardware:
Hardware or that hardware
  1. The physical principle of hardware remains unchanged. The same as bare metal debugging is to pull up and pull down the led to light up
  2. The hardware operation interface (register) remains unchanged, and the control register and data register remain unchanged
  3. The hardware operation code remains unchanged, point to the register address with a pointer, and then write the data in

difference:

  1. Register address. The bare metal part uses the real physical address. Now it needs to use the virtual machine address. It uses the virtual address corresponding to the physical address in the kernel virtual address space. The register address is determined during CPU design and found in the datasheet
  2. Different programming methods. It is customary to use functions to directly operate registers in bare metal machines, and then use io encapsulated registers in kernel to operate registers. In this way, it has certain portability
Kernel virtual mapping method:
  1. Why do I need virtual address mapping

    1. As long as the MMU is turned on, it uses the virtual address. When it is turned off, it is the physical address, and there is no real distinction
  2. There are two sets of virtual address mapping methods in the kernel: dynamic and static

  3. Characteristics of static mapping method:

    1. When the kernel is started, it is hard coded in the form of code (written in code). If you want to change, you must change the source code and recompile the kernel. Establish a static mapping table when the kernel is started and destroy it when the kernel is shut down. The middle is always valid. For the transplanted kernel, it is there whether you use it or not, which is valid during the whole kernel operation
  4. Characteristics of dynamic mapping:

    1. There is no establishment in advance. The driver dynamically establishes, uses and destroys the mapping at any time according to the needs. The mapping is short-term and temporary,
    2. Apply for dynamic mapping during program operation, use it, and destroy it when not in use
    How to select a virtual address mapping method:
    1. The two mappings are not exclusive and can be used at the same time
    2. Static mapping is similar to global variables in C language, and dynamic mapping is similar to malloc applying for memory
    3. The advantage of static mapping is high execution efficiency, that is, static mapping always exists, and there is no need to apply for dynamic overhead when it is used. The disadvantage is that it always occupies the virtual address space. The advantage of dynamic mapping is to use the virtual address space as needed, and the disadvantage is that before and after each use, you need code to establish mapping & destroy mapping (you have to learn to use those kernel functions)
Static mapping table
  1. The location and file name of the static mapping table in different versions of the kernel may be different

  2. Different soc static mapping table locations may have different file names

  3. The so-called mapping table is actually the macro definition of the header file

    Static mapping table in Samsung version kernel:
    1. The main mapping table is located at: arch / arm / plat-s5p / include / plat / map-s5p h

    When the CPU arranges the register address, it is not randomly distributed in random order, but distinguished according to the module. Each module has many registers whose addresses are continuous. Therefore, when the kernel defines the register address, it first finds the base address, and then uses the and address + offset to find a specific register

    ​ map-s5p.h defines the register base addresses of several modules to be used. If we need it in the future, we can add the base addresses we want to use

    ​ map-s5p.h is the virtual address of the register base address of the module

    2. The virtual address base address is defined in: arch / arm / plat sampling / include / plat / map base h

    ​ #define S3C_ADDR_BAS (0xFD000000) / / the virtual address base address of the static mapping table determined during Samsung migration. All virtual addresses in the table are specified by this address + offset

    3. The main mapping table related to GPIO is located at: arch / arm / mach-s5pv210 / include / Mach / regs GPIO h

    All relevant gpio registers are defined

    The table is the definition of the base address of each port of GPIO

    4. The specific register definition of GPIO is located in: arch / arm / mach-s5pv210 / include / Mach / GPIO bank h

Static mapping operation LED

Refer to the operation method in bare metal machine and add the operation code of led

Then the init/exit function turns on and off the LED respectively

We finally need to turn on and off the lights in read and write

(1) First define the control interface between the application and the driver, which is defined by yourself. For example, when the application writes "on", the light is on and the "off" light is off

(2) We need to make a simple judgment through the application layer

module_test.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#include <linux/string.h>

int major;
char kbuf[100];

#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT

#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)

static int test_open(struct inode *inode, struct file *file)
{
	printk("test_open-----");
	return 0;
}

static int test_release(struct inode *inode, struct file *file)
{
	printk("test_release----------");
	return 0;
}


static ssize_t test_read( struct file *file, char *buf, size_t count, loff_t *ppos )
{
	int ret = -1;
	printk("test_read----------");
	//Use this function to copy the content in the ubuf transmitted from the application layer to a buf in the driver space
	ret = copy_to_user(buf, kbuf, count);
	if(ret)
	{
		printk("copy to user failed\n");
		return -EINVAL;
	}
	
	
	return 0;
}

//The essence of write function is to copy the data passed from the application layer to the kernel, and then write it to the hardware in the correct way to complete the operation
static ssize_t test_write( struct file *file, const char *buf, size_t count,
						  loff_t *ppos )
{
	int ret = -1;
	rGPJ0CON = 0x11111111;
	
	printk("test_write----------");
	
	memset(kbuf, 0, sizeof(kbuf));
	//Use this function to copy the content in the ubuf transmitted from the application layer to a buf in the driver space
	ret = copy_from_user(kbuf, buf, count);
	if(ret)
	{
		printk("copy from user failed\n");
		return -EINVAL;
	}

	if(kbuf[0] == '1')
	{
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
	}
	else if(kbuf[0] == '0')
	{
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
	}
	printk("copy from user successful\n");
	
	//The data from the application layer can be used to really manipulate the hardware
	return 0;
}

//Define a file_operation structure
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,  

	.open		= test_open,     //In the future, the application will open the device and actually call it
	.release	= test_release,  //This is the function corresponding to this close
	.read       = test_read,
	.write      = test_write,
};

static int __init chrdev_init(void)
{
	//Register file here_ Operation structure, in module_ Register the character device driver in the function called by init macro
	printk("chrdev_init\n");
	if ((major = register_chrdev(0, "test", &test_fops)) < 0) {
		printk("adb: unable to get major %d\n", major);
		return -1;
	}
	printk("register_chrdev successful\n");


	
	return 0;
}

static void __exit chrdev_exit(void)
{
	unregister_chrdev(major, "test");
	//In module_ Unregister the character device driver in the function called by the exit macro
	printk("chrdev_exit\n");
}

module_init(chrdev_init);
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");

app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#Define file "/ dev / test" / / mknod creates the name of the device file
char buf[100];

int main(void)
{
	int fd = -1;
	int i;
	fd = open(FILE, O_RDWR);
	if(fd < 0)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	
	printf("open %s success...\n", FILE);

	while(1)
	{
		memset(buf, 0, sizeof(buf));
		printf("please enter on|off\n");
		scanf("%s", buf);
		if(!strcmp(buf, "on"))
			write(fd, "1", 1);
		else if(!strcmp(buf, "off"))
			write(fd, "0", 1);
		else if(!strcmp(buf, "flash"))
		{
			for(i = 0; i< 3; i++)
			{
				write(fd, "1", 1);
				sleep(1);
				write(fd, "0", 1);
				sleep(1);
			}
		}
		else if(!strcmp(buf, "quit"))
			break;
	}
	
	//Close file
	close(fd);
	return 0;
}

Dynamic mapping operation led:
  1. How to establish dynamic mapping

    1. request_mem_region applies to the kernel and reports the memory resources to be mapped
    2. ioremap is really used to realize mapping. Pass it a physical address and return the mapped virtual address
    3. Note that when creating a mapping, you must first apply for mapping, then use it, and then release it after using it
  2. How to destroy dynamic mapping

    1. iounmap
    2. release_mem_region

    module_test.c

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <asm/uaccess.h>
    #include <mach/regs-gpio.h>
    #include <mach/gpio-bank.h>
    #include <linux/string.h>
    #include <linux/io.h>
    #include <linux/ioport.h>
    
    
    int major;
    char kbuf[100];
    
    #define GPJ0CON S5PV210_GPJ0CON
    #define GPJ0DAT S5PV210_GPJ0DAT
    
    #define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
    #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
    
    #define GPJ0CON_PA 0xe0200240
    #define GPJ0DAT_PA 0xe0200244
    
    unsigned int *pGPJ0CON;
    unsigned int *pGPJ0DAT;
    
    static int test_open(struct inode *inode, struct file *file)
    {
    	printk("test_open-----");
    	return 0;
    }
    
    static int test_release(struct inode *inode, struct file *file)
    {
    	printk("test_release----------");
    	return 0;
    }
    
    
    static ssize_t test_read( struct file *file, char *buf, size_t count, loff_t *ppos )
    {
    	int ret = -1;
    	printk("test_read----------");
    	//Use this function to copy the content in the ubuf transmitted from the application layer to a buf in the driver space
    	ret = copy_to_user(buf, kbuf, count);
    	if(ret)
    	{
    		printk("copy to user failed\n");
    		return -EINVAL;
    	}
    	
    	
    	return 0;
    }
    
    //The essence of write function is to copy the data passed from the application layer to the kernel, and then write it to the hardware in the correct way to complete the operation
    static ssize_t test_write( struct file *file, const char *buf, size_t count,
    						  loff_t *ppos )
    {
    	int ret = -1;
    	rGPJ0CON = 0x11111111;
    	
    	printk("test_write----------");
    	
    	memset(kbuf, 0, sizeof(kbuf));
    	//Use this function to copy the content in the ubuf transmitted from the application layer to a buf in the driver space
    	ret = copy_from_user(kbuf, buf, count);
    	if(ret)
    	{
    		printk("copy from user failed\n");
    		return -EINVAL;
    	}
    
    	if(kbuf[0] == '1')
    	{
    		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
    	}
    	else if(kbuf[0] == '0')
    	{
    		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
    	}
    
    
    	printk("copy from user successful\n");
    	
    	//The data from the application layer can be used to really manipulate the hardware
    	return 0;
    }
    
    //Define a file_operation structure
    static const struct file_operations test_fops = {
    	.owner		= THIS_MODULE,  
    
    	.open		= test_open,     //In the future, the application will open the device and actually call it
    	.release	= test_release,  //This is the function corresponding to this close
    	.read       = test_read,
    	.write      = test_write,
    };
    
    static int __init chrdev_init(void)
    {
    	//Register file here_ Operation structure, in module_ Register the character device driver in the function called by init macro
    	printk("chrdev_init\n");
    	if ((major = register_chrdev(0, "test", &test_fops)) < 0) {
    		printk("adb: unable to get major %d\n", major);
    		return -1;
    	}
    	printk("register_chrdev successful\n");
    
    	//Use dynamic mapping to manipulate registers
    	if(!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
    		return -EINVAL;
    	
    	if(!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
    		return -EINVAL;
    	
    	pGPJ0CON = ioremap(GPJ0CON_PA, 4);
    	pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
    	
    	*pGPJ0CON  = 0x11111111;
    	*pGPJ0DAT  = ((0<<3) | (0<<4) | (0<<5));  //bright
    	return 0;
    }
    
    static void __exit chrdev_exit(void)
    {
    	unregister_chrdev(major, "test");
    	//Unmap
    	iounmap(pGPJ0CON);
    	iounmap(pGPJ0DAT);
    	
    	release_mem_region(GPJ0CON_PA, 4);
    	release_mem_region(GPJ0DAT_PA, 4);
    	//In module_ Unregister the character device driver in the function called by the exit macro
    	printk("chrdev_exit\n");
    }
    
    module_init(chrdev_init);
    module_exit(chrdev_exit);
    
    MODULE_LICENSE("GPL");
    
    

Topics: Linux kernel Linux Driver