i.MX6ULL(16) linux device driver

a brief introduction

A Linux device driver refers to a software module that drives the Linux kernel to communicate with a hardware device. Device drivers are usually divided into two categories: character device drivers and block device drivers.

The main functions of the device driver include:

  1. Device initialization: When the system starts, the device driver needs to initialize the corresponding hardware device, set the parameters of the device's registers and interfaces, so as to ensure that the device can work normally.
  2. Device control: The device driver needs to provide some interfaces to control various operations of the device, such as opening the device, reading data, writing data, and closing the device.
  3. Interrupt handling: The device driver needs to handle the interrupt request of the hardware device, and execute the corresponding interrupt processing program when the interrupt occurs, so as to respond to various events and requests of the device in time.
  4. Data transmission: The device driver needs to implement the data transmission function, including operations such as reading data from the hardware device and writing data to the hardware device.
  5. Error handling: The device driver needs to handle errors and exceptions that occur in the device, such as device read and write errors, interrupt loss, etc.

1.1 Device driver classification

1.1.1 Block Device Driver

A block device driver refers to a driver program that communicates with a device in units of blocks, such as storage device drivers such as hard drives, solid state drives, and USB flash drives.

Block device drivers usually adopt a hierarchical structure, including the following levels:

  1. Device Driver Layer: This layer communicates directly with the hardware device and controls various operations of the device.
  2. Storage volume manager layer: This layer is responsible for managing storage volumes (such as hard disk partitions, logical volumes, etc.) and providing access interfaces to volumes.
  3. File system layer: This layer provides a file system interface to the volume, enabling users to access and manage files according to the directory structure of the file system.

1.1.2 Character device driver

The character device is the most basic type of device driver in the Linux driver. The character device is a byte, according to the byte
For devices that perform read and write operations on streams, data is read and written in sequential order. For example, our most common lighting, buttons, IIC , SPI ,
LCD, etc. are all character devices, and the drivers for these devices are called character device drivers.

I2C/SPI devices can be character devices, block devices, or network devices, depending on their functionality and application scenarios.

However, for some I2C devices, they may be classified as other types of devices such as block devices or network devices. For example, certain I2C memories such as EEPROMs can be considered examples of block devices because they transfer data in blocks. Other I2C devices, such as temperature sensors or light sensors, can be considered network devices because they transmit data in packets.

Therefore, not all I2C/SPI devices are character devices , but may be classified into different types of devices according to their functions and application scenarios. Different types of devices require different drivers to access and control them.

The call of the driver program by the Linux application program is shown in Figure 40.1.1

In Linux, everything is a file. After the driver is successfully loaded, a corresponding file will be generated in the " /dev " directory, which should be
The program can be implemented by performing corresponding operations on the file named " /dev/xxx " (xxx is the name of the specific driver file ) .
Now operate on the hardware. For example, there is now a driver file called /dev/led , which is the driver file of the LED light. app
The program uses the open function to open the file /dev/led , and uses the close function to close the file /dev/led after use . open and close are functions to turn on and off the led driver. If you want to turn on or turn off the led , use the write function to operate, that is, to write data to the driver. This data is the control parameter to turn off or turn on the led. If you want to get
For the state of the led light, use the read function to read the corresponding state from the driver.
Applications run in user space, while Linux drivers are part of the kernel, so drivers run in kernel space.
When we want to implement operations on the kernel in user space, such as using the open function to open the /dev/led driver, because using
User space cannot directly operate on the kernel, so a method called "system call" must be used to implement
Time "trapped" into the kernel space, so as to realize the operation of the underlying driver.

Two device driver principles

2.1 Loading and unloading of the driver module

2.1.1 Loading

There are two ways to run the Linux driver. The first one is to compile the driver into the Linux kernel, so that when the Linux kernel starts
It will automatically run the driver when it starts. The second is to compile the driver into a module ( the extension of the module under Linux is .ko) , in
After the Linux kernel starts, use the " insmod " command to load the driver module. When debugging the driver, generally choose to compile it
It is a module, so that after we modify the driver, we only need to compile the driver code, instead of compiling the entire Linux code.
And when debugging, you only need to load or unload the driver module
module_init(xxx_init); //register module loading function
module_exit(xxx_exit); //register module uninstall function

After the driver is compiled, the extension is .ko . There are two commands to load the driver module: insmod and modprobe

insmod command cannot resolve module dependencies
For example, if drv.ko depends on the first.ko module, you must first use the insmod command to load the first.ko module, and then load the drv.ko module.
modprobe will analyze the dependencies of the modules, and then load all dependent modules into the kernel,
insmod drv.ko
modprobe   drv.ko

2.1.2 Uninstall

To uninstall the driver module, use the command " rmmod ". For example, to uninstall drv.ko ,
rmmod drv.ko
modprobe -r drv.ko //
Use the modprobe command to uninstall other modules that the driver module depends on, provided that these dependent modules are no longer
It is used by other modules, otherwise you cannot use modprobe to unload the driver module. So for module uninstallation, it is recommended to use the rmmod command

2.2  Address Mapping MMU

The full name of MMU is called Memory Manage Unit, which is the memory management unit. In the old version of Linux, the processor must have an MMU , but now
The Linux kernel already supports processors without an MMU . The main functions of the MMU are as follows:
①. Complete the mapping from virtual space to physical space.
②. Memory protection, set the access rights of the memory, and set the buffering characteristics of the virtual storage space
For a 32 -bit processor, the virtual address range is 2^32=4GB
When the Linux kernel starts, it will initialize the MMU , set up the memory map, and after setting up, all CPU accesses are virtual
proposed address.
For example, the address of the multiplexing register IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 of the GPIO1_IO03 pin of I.MX6ULL is 0X020E0068 .
If the MMU is not enabled, write data directly to the register address 0X020E0068 to configure the multiplexing function of GPIO1_IO03 .
Now the MMU is turned on and the memory mapping is set, so it is not possible to directly write data to the address 0X020E0068 . We must get the virtual address corresponding to the physical address 0X020E0068 in the Linux system, which involves the conversion between physical memory and virtual memory, and two functions need to be used: ioremap and iounmap .

2.2.1  ioremap function

The ioremap function is used to obtain the virtual address space corresponding to the specified physical address space. It is defined in the arch/arm/include/asm/io.h file and is defined as follows:

示例代码 41.1.1.1 ioremap 函数
 #define ioremap(cookie,size) __arm_ioremap((cookie), (size), 
MT_DEVICE)

 void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, 
unsigned int mtype)
 {
 return arch_ioremap_caller(phys_addr, size, mtype,
__builtin_return_address(0));
 }
This function has three parameters and a return value. The meanings of these parameters and return value are as follows:
phys_addr : The physical starting address to map.
size : The size of the memory space to be mapped.
mtype : the type of ioremap , you can choose MT_DEVICE , MT_DEVICE_NONSHARED ,
MT_DEVICE_CACHED and MT_DEVICE_WC , the ioremap function selects MT_DEVICE .
Return value: A pointer of __iomem type, pointing to the first address of the mapped virtual space.

2.2.2  iounmap function

When uninstalling the driver, you need to use the iounmap function to release the mapping made by the ioremap function. The iounmap function originally
type as follows
void iounmap (volatile void __iomem *addr)
iounmap has only one parameter addr , which is the first address of the virtual address space to be unmapped.

2.2.3 I/O memory access functions

Linux indirectly manipulates physical addresses by manipulating virtual addresses

1. Read operation function
 u8 readb(const volatile void __iomem *addr)
 u16 readw(const volatile void __iomem *addr)
 u32 readl(const volatile void __iomem *addr)

write operation function

 void writeb(u8 value, volatile void __iomem *addr)
 void writew(u16 value, volatile void __iomem *addr)
 void writel(u32 value, volatile void __iomem *addr)

2.3 Device number

Each device in Linux has a device number. The device number is composed of two parts: the major device number and the minor device number. The major device number represents a specific driver, and the minor device number represents each device that uses this driver.

2.3.1 Composition

Linux provides a data type named dev_t to represent the device number, dev_t is defined in the file include/linux/types.h,
The upper 12 bits of the dev_t __u32 type are the major device number, and the lower 20 bits are the minor device number. Therefore Linux
The main equipment number in the system ranges from 0 to 4095 , so you must not exceed this range when selecting the main equipment number.

2.3.2 Device number allocation and cancellation

static allocation
Some commonly used device numbers have been allocated by Linux kernel developers. For specific allocation content, please refer to Documentation/devices.txt . It’s not that we can’t use the main device number that has been allocated by the kernel developer. Whether it can be used or not depends on whether the main device number is used during the operation of our hardware platform. Use the “cat /proc/devices” command You can view all the device numbers that have been used in the current system.
dynamic allocation
Static allocation of device numbers requires us to check all used device numbers in the current system, and then pick one that is not used
of. Moreover, static allocation of device numbers is likely to cause conflicts. The Linux community recommends using dynamic allocation of device numbers.
Apply for a device number before you sign the device, and the system will automatically give you an unused device number, thus avoiding conflicts.
Just release the device number when uninstalling the driver. The application function for the device number is as follows:
application function
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
baseminor : the starting address of the minor device number, alloc_chrdev_region can apply for a continuous multiple device numbers, this
The major device numbers of these device numbers are the same, but the minor device numbers are different, and the minor device numbers start with baseminor as the starting address.
increase. Generally, baseminor is 0, that is to say, the minor device number starts from 0.
count : The number of device numbers to apply for.
registration function
If the major device number and minor device number of the device are given, use the following function to register the device number:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
3 logout function
After the character device is unregistered, the device number must be released. The device number release function is as follows:
void unregister_chrdev_region(dev_t from, unsigned count)
from : The device number to release.
count : Indicates the number of device numbers to be released starting from from .

2.4  New character device registration method

2.4.1 , character device structure

Use the cdev structure to represent a character device in Linux , and the cdev structure is in the include/linux/cdev.h file
is defined as follows:
 struct cdev {
 struct kobject kobj;
 struct module *owner;
 const struct file_operations *ops;
 struct list_head list;
 dev_t dev;
 unsigned int count;
 };
There are two important member variables in cdev : ops and dev , these two are the collection of character device file operation functions
file_operations and device number dev_t .

2.4.2 , cdev_init function

After the cdev variable is defined , it must be initialized with the cdev_init function. The prototype of the cdev_init function is as follows:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
The parameter cdev is the cdev structure variable to be initialized , and the parameter fops is the set of character device file operation functions.
The sample code for initializing cdev variables using the cdev_init function is as follows:

/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

	
/* 设备操作函数 */
static struct file_operations newchrled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};


/* 2、初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);

2.4.3, cdev_add function

The cdev_add function is used to add a character device (cdev structure variable ) to the Linux system ,
First use the cdev_init function to complete the initialization of the cdev structure variable, and then use the cdev_add function to add this character device to the Linux system.
The prototype of the cdev_add function is as follows:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
The parameter p points to the character device (cdev structure variable ) to be added , and the parameter dev is the device number used by the device, see
The count count is the number of devices to add.

2.4.4, cdev_del function

When uninstalling the driver, be sure to use the cdev_del function to delete the corresponding character device from the Linux kernel, cdev_del
The function prototype is as follows:
void cdev_del(struct cdev *p)
The parameter p is the character device to be deleted.

2.5  Automatically create device nodes

In the previous Linux driver experiment, when we use modprobe to load the driver, we need to use the command
" mknod " manually creates device nodes. This section will explain how to automatically create device nodes and implement them in the driver
After the function of automatically creating device nodes, if the driver module is successfully loaded using modprobe , it will automatically be in the /dev directory
Create the corresponding device file.

2.5.1 mdev mechanism

udev is a user program. Under Linux , udev is used to create and delete device files. udev can check
Detect the status of hardware devices in the system, and create or delete device files according to the status of hardware devices in the system. such as using
After the modprobe command successfully loads the driver module, it will automatically create the corresponding device node file in the /dev directory , use
After the rmmod command uninstalls the driver module , the device node files in the /dev directory are deleted . Use busybox to build the root file
system, busybox will create a simplified version of udev—mdev , so in embedded Linux we use
mdev to realize the automatic creation and deletion of device node files, and the hot plug events in the Linux system are also managed by mdev.
The following statement in the /etc/init.d/rcS file:
echo /sbin/mdev > /proc/sys/kernel/hotplug
How to realize the automatic creation and deletion of device file nodes through mdev ?
2.5.1.1  Creating and deleting classes
The work of automatically creating device nodes is done in the entry function of the driver, usually after the cdev_add function.
Add automatic creation of device node related codes. First, create a class class, class is a structure, defined in the file
include/linux/device.h inside. class_create is a class creation function, class_create is a macro definition,
struct class *class_create (struct module *owner, const char *name)
When uninstalling the driver, the class needs to be deleted. The class deletion function is class_destroy , and the function prototype is as follows:
void class_destroy(struct class *cls);
2.5.1.2 Create Device
Create a device below the class using the device_create function
struct device *device_create(struct class
*class,
struct device *parent,
dev_t
devt,
void
*drvdata,
const char
*fmt, ...)
device_create is a variable parameter function, the parameter class is the class under which the device is to be created; the parameter parent is the parent
Device, generally NULL , that is, there is no parent device; the parameter devt is the device number; the parameter drvdata is the device that may be used
Some data, generally NULL ; parameter fmt is the device name, if fmt=xxx is set, /dev/xxx will be generated
this device file. The return value is the created device.
Similarly, when uninstalling the driver, you need to delete the created device. The device delete function is device_destroy , and the original function is
The type is as follows:
void device_destroy(struct class *class, dev_t devt)

2.6  Set file private data

Each hardware device has some attributes, such as the main device number (dev_t) , class (class) , device (device) , switch state (state)
Wait, you can write all these properties as variables when writing the driver, and add the device structure as private data to the device file when writing the driver open function, as follows
/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

2.7 Generic device driver creation template   reference example

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>




/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};

struct newchrled_dev newchrled;	/* led设备 */



/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations newchrled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	...

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) {		/*  定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */
		newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */
	}
	printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);	
	
	/* 2、初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class)) {
		return PTR_ERR(newchrled.class);
	}

	/* 5、创建设备 */
	newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.device)) {
		return PTR_ERR(newchrled.device);
	}
	
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{

    ....
	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
// MODULE_VERSION("4.1.15-g3dc0a4b SMP preempt mod_unload modversions ARMv7 p2v8")

Guess you like

Origin blog.csdn.net/TyearLin/article/details/131745751