Embedded Linux Development——Note (2)——Development process of character device driver

Copyright Mentioned Ahead

This article is my notes on learning Linux device driver development, and it is a summary of the tutorial on reading punctual atoms , soCopyright will be marked as reprint. usFirst sort out the process of developing a character device driver, and then analyze the development process of the simplest device driver LED on this basis. Finally, as a supplement, add someWriting new driversprocess.

Let me make some complaints about the tutorial on punctuality, the logic is too messy, and I go around when I watch it.

1. Important steps of character device driver - top-down view

1.1 Loading and unloading of the driver module

Suppose we already have a character device driver module, after the driver is compiledThe module suffix is ​​.ko, there are two ways to load the driver module: insmod and modprobe , their basic usage is as follows:

# insmod加载模块
insmod drv.ko
# rmmod卸载模块
rmmod drv.ko

# -------------------------------

# modprobe加载模块
modprobe drv.ko
# modprobe 卸载模块
modprobe -r drv.ko

insmod and rmmod are paired, they cannot automatically analyze module dependencies when loading modules, and modprobe can, so inModprobe is recommended when loading driver modules. andWhen using modprobe -r to uninstall a module, you must first uninstall other modules that depend on it, so the rmmod command is recommended when uninstalling modules .

1.2 Registration and deregistration of character devices

When we write the driver code, we need to use module_init and module_exit to register the module entry and exit functions with the Linux kernel . When using the above insmod or modprobe command to load or unload the driver module,The entry and exit functions registered with module_init and module_exit will be executed automatically, to complete the initialization or logout of the device, so let's take a closer look at the writing of the entry and exit functions.

The thing to do in the population and export functions is to initialize and register the character device, or to log out . In the old version of the driver, the function to complete the corresponding functions of registration and cancellationThey are register_chrdev and unregister_chrdev respectively

// @param major:要注册的设备的主设备号
// @param name:要注册的设备名,帮助debug
// @param fops:设备驱动包含的动作结构体
static inline int register_chrdev(
									unsigned int major, 
									const char *name,
									const struct file_operations *fops);

//---------分割线-----------
// @param major: 要注销的设备的主设备号
// @param name: 要注销的设备的名字
static inline void unregister_chrdev(
									unsigned int major, 
									const char *name);

1.3 Write the specific driver function of the device

When we register the device onThe register_chrdev function is used, this function needs to receive a structure fops describing the operations supported by the device to describe the operations that need to be supported in the device driver . So here we need to write our own code to implement a series of operations in the driver such as opening files, reading and writing files, etc. This isThe most difficult and ever-changing part of developing a driving device,we need toRead the manual by yourself and configure the registers correctly to complete these functions

After completing the writing and implementation of specific functions, use syntax similar to the followingRegister the function in a structure of type file_operations, and then pass in this structure when calling the register_chrdev function, the following isvirtual character device driverThe function registration code, as a reference.

static struct file_operations test_fops = {
    
    
	.owner = THIS_MODULE, 
	.open = chrtest_open,
	.read = chrtest_read,
	.write = chrtest_write,
	.release = chrtest_release,
};

1.4 Add LICENSE and author information

After developing the above process, most of our work has been completed, but finally we need to add the LICENSE information that this driver follows and the author's related information. These information are defined using the following macros:

// 给驱动程序附加LICENSE和作者相关信息,其中LICENSE是必须的
MODULE_LICENSE();
MODULE_AUTHOR();

// 例子:添加模块LICENSE信息和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tom");

2. Device number and its allocation

In the Linux system, each device in Linux has a device number, and the device number is determined bymajor and minor numbersconsists of two parts,The major device number indicates a specific driver, and the slave device number indicates each device that uses this driver. The essence of the device number is aUnsigned integer data (unsigned int32),inThe upper 12 bits are the master device number, and the lower 20 bits are the slave device number. Therefore, the value range of the major device number is 0~4095, and it must not exceed this range when selecting the major device number .

When assigning the main device number (such as when we register the device above, in the register_chrdev functionThe first parameter to be passed in) can have the following two methods:

  • Static allocation : usecat /proc/devices commandYou can list all currently occupied device numbers, and thenManually select an unoccupied major device number
  • Dynamic allocation (recommended!) : we canUse alloc_chrdev_region and unregister_chrdev_regionThese two functions allow Linux to automatically apply for and reclaim the device number for us. The prototypes of these two functions are as follows, note that they can both be used to allocate and reclaim a range of device numbers :
// @param dev: 申请到的主设备号,带出参数
// @param baseminor: 从设备号开始值,一般从0开始
// @param count: 要申请的设备号数量
// @param name: 设备名称 
int alloc_chrdev_region( 	dev_t *dev, 
							unsigned baseminor, 
							unsigned count, 
							const char *name);
// -------------------------------------------------
// @param from: 要释放的初始主设备号
// @param count: 要释放的设备号数量
void unregister_chrdev_region(dev_t from, unsigned count);

3. Examples and more details - take the development of led driver as an example

3.1 ioremap and iounmap functions

Above we briefly stated the development process of the character device driver, and now use an example to connect the above process, this example is the simplest character device:LED lights.We light up the LED light on the board, which is essentially done by outputting a high level through a GPIO pin, so what we need to do now is actually very simple: write the value (level) to the address specified by MMIO, that is, can .

But because of the existence of the MMU, it is inevitable to avoid the key link of mapping virtual addresses to physical addresses , which is automatically completed by the MMU.The problem now is that we know the physical address of the port (just like when developing on bare metal), but we don't know its corresponding virtual address, so we can't access this address through pointers in the program. That is, we cannot realize the process of anti-mapping. In order to help us solve this problem, in the Linux systemTwo functions are provided to help us map the physical address of IO to the virtual address space: ioremap and iounmap

The former is used to obtain the virtual address space corresponding to the specified physical address space , and the latter is used to release the mapping made by ioremap .

// ioremap完成物理地址到虚拟地址的反映射
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

// ioremap函数本质上是以下__arm_ioremap函数的简单封装
// @param phys_addr : 要反映射的物理地址起始位置
// @param size : 要反映射的地址空间大小
// @param mtype : ioremap的类型,默认传入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));
}

The following is the function declaration of iounmap:

// 解除ioremap构建的映射关系,只需要将ioremap返回的指针传入即可
// @param addr : 要释放的IO设备在虚拟地址空间中的首地址
void iounmap (volatile void __iomem *addr)

Maybe you have noticed the strange modifier __iomem, and there are many articles behind this symbol. Generally speaking,It indicates that the address space currently pointed to belongs to the IO address space. Also, pointers with the __iomem modifier are not allowed to be dereferenced.So, since it cannot be dereferenced, how do we access this address? Linux requires us to access pointers with __iomem modification through the following functions , otherwise the compiler may report errors or warnings:

// 以不同大小来读取__iomem指针
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

// 以不同大小来写入__iomem指针
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

3.2 Example - Analysis of the initialization code of the device

As for the writing of the driver program, it is actually very simple. The more interesting thing here is the initialization function, and the source code is posted here. It can be seen that firstly, ioremap is used in the code to map the physical address to the virtual address, and then the configuration of the GPIO register is performed as in the bare metal development . Note that the reading and writing of the registers are all done through the above-mentioned special read and write functions . These are It is a very routine operation, no further explanation will be given.

at lastUse the register_chrdev function to register the device driver, thus completing the entire initialization process.The led_init function will be registered with module_init as an entry function, so that this function will be executed when we load the driver module.

static int __init led_init(void)
{
    
    
	int retvalue = 0;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 6、注册字符设备驱动 */
	retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
	if(retvalue < 0){
    
    
		printk("register chrdev failed!\r\n");
		return -EIO;
	}
	return 0;
}

Corresponding to the above process, we also have to have an export function, the code is as follows,First use iounmap to unmap, and finally unregister the driver module

static void __exit led_exit(void)
{
    
    
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/* 注销字符设备驱动 */
	unregister_chrdev(LED_MAJOR, LED_NAME);
}

3.3 Board verification

We compile the code written above (in fact, the code written by the punctual atom... Khan) to get the followingThe driver module with the suffix .ko (kernel object). After getting this module, map it directly to the development board through nfs, and put it onin the loadable kernel modules folder, the name of this folder varies with the kernel version. My path is /lib/modules/4.1.15-g3dc0a4b . Note that the Linux kernel source version used when compiling the kernel driver module must also be /4.1.15- g3dc0a4b, otherwise an error will be reported. For details, please refer to this WeChat article . I stepped on a pit during the board verification process.

The verification steps are as follows:

# 1.此命令用于检测模块之间的相互依赖性,应该在调用modprobe之前执行
depmod

# 2.然后调用modprobe进行驱动模块加载
modprobe led.ko

# 这里可能会报错
# root@ATK-IMX6U:/lib/modules/4.1.15-g3dc0a4b# modprobe led.ko
# modprobe: FATAL: Module led.ko not found in directory /lib/modules/4.1.15-g3dc0a4b
# 这种情况只需要去掉.ko的后缀即可

# 3.为设备创建设备节点
# c表示当前设备是字符设备,200和0分别是主设备号和从设备号
mknod /dev/led c 200 0

In fact, after executing the modprobe command, the driver module has been loaded, and we can confirm it through the cat /proc/devices command, as shown in the figure below. andCreating a device node is just to make it easier for us to use this device, just like reading and writing a file
insert image description here

4. Compilation of the new version of the driver

Here, punctual atom introduces a new character device driver development process. Here we simply write out the differences from the traditional process. I summarize the following points:

4.1 Optimization 1 - the allocation process of device numbers

In the traditional development process, we use the register_chrdev function to manually specify the device number. Before that, weYou must use cat /proc/devices to determine whether a certain device number is used, which is relatively inconvenient and inflexible.

retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);

In the new version of the device driver, we introduced theTwo functions such as alloc_chrdev_region and register_chrdev_regionTo apply for a device number from the Linux system. The difference between these two functions is that the alloc_chrdev_region function isLet the Linux system automatically assign an idle device number, and register_chrdev_region is the user's application to the Linux system for the specified device number. This is done in the new version of the driver code:

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);	
		// ...
	}

That is to say, if the user specifies the device number in advance, then use the register_chrdev_region function to apply directly, otherwise use the alloc_chrdev_region function to let the Linux system automatically help us allocate a device, andBring out directly through the amount of newchrled.devid

Of course, the device number must be released after applying for the device number from the system, so the exit function code is as follows, we can see that there is a process of releasing the device number, this operation is done using the unregister_chrdev_region function, this functionSpecifically used to release the device number allocated by the alloc_chrdev_region and register_chrdev_region functions

static void __exit led_exit(void)
{
    
    
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

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

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

4.2 Optimization 2 - new character device registration method

In the previous version, we used the register_chrdev function to register a character device with the kernel. In the writing of the new version of the driver, this process becomesA process connected in series using the special structure of cdev. That is to say, there is a special data structure in the Linux kernel to represent character devices, namely cdev:

struct cdev {
    
    
	struct kobject kobj;
	struct module *owner;				// 一般是THIS_MODULE
	const struct file_operations *ops;	// 字符设备的各个动作都记录在此
	struct list_head list;		
	dev_t dev;							// 设备号
	unsigned int count;
};

Our action of registering a device with the kernel becomes the following steps:

// 1.使用cdev_init函数初始化一个cdev设备
// @param cdev : 指向设备的指针
// @param fops : 设备支持的驱动操作
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

// 2.使用cdev_add函数将上面初始化好的字符设备添加到内核中
// @param p : 指向字符设备的指针
// @param dev : 设备号
// @param count : 要添加的设备数量
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

// 3.在出口函数中,删除掉这个字符设备,将设备指针传入即可
// @param p : 指向字符设备的指针
void cdev_del(struct cdev *p)

4.3 Optimization 3 - Automatically create device nodes

In the old version of the driver code, we wereAfter getting the .ko module, you have to use the mknod command to create the device node yourself, but in the new version of the code, you can use the mdev mechanism to automatically create the node. Here we do not understand the mdev mechanism in depth, but directly give the development process. We need to use the class_create function and device_create function to create this device node after the cdev_add operation :

// 1.在cdev_add操作之后创建一个类
// @param owner :此模块属于什么机构,一般写为THIS_MODULE
// @param name : 类的名称
struct class *class_create (struct module *owner, const char *name)

// 2.使用device_create函数创建一个设备
// @param class : 设备所属的类,就用上面创建出来的类
// @param parent : 该设备的父设备,如果是单一模块开发,则设为NULL
// @param devt : 设备号
// @param drvdata : 设备的一些附加数据,一般设为NULL
// @param fmt : 设备名字,传入“xxx”时,会生成名为/dev/xxx的设备
struct device *device_create(	struct class *class, 
 								struct device *parent,
 								dev_t devt, 
 								void *drvdata, 
 								const char *fmt, ...)

// 2.与上述函数对应的,在出口函数中应该将创建的设备和类都销毁
void class_destroy(struct class *cls);
void device_destroy(struct class *class, dev_t devt);

4.4 Protection of data

After the introduction of new functions and structures above, there is now a lot of information associated with a function, first of allA character device is abstracted into a structure like cdev, it also belongs to a class class, and as a device itself, it also has its own related information. Therefore, in order to ensure the logical relevance of this information, we generally declare it as a structure and use it, as follows:

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

// 使用上面的结构体声明一个设备,这就是我们要操作的设备实例
// 注意,最好将它注册进flip->private_data
struct newchrled_dev newchrled;	/* led设备 */

Such a structure actually abstracts the entire device we defined. In the open function of the device, its address is generally registered in the flip->private_data field . For example, the new version of the driver code has the following implementation:

// 将led设备对应的结构体注册到private_data字段
static int led_open(struct inode *inode, struct file *filp)
{
    
    
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

4.5 Summary

Here is the code implementation of the exit function and entry function, pay attention to compare with the code implementation of the old version:

4.5.1 Implementation of the new entry function

/* 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设备 */

static int __init led_init(void)
{
    
    
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);
	
	// ----------------------------以下代码和老版本实现不一致----------------------
	// ---------------------------4.1 优化1——设备号的分配过程----------------------------
	/* 注册字符设备驱动 */
	/* 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);	
	
	// ---------------------------4.2 优化2——新的字符设备注册方法------------------------
	/* 2、初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
	
	// ---------------------------4.3 优化3——自动创建设备节点------------------------
	/* 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;
}

4.5.2 Implementation of the new export function

static void __exit led_exit(void)
{
    
    
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
	
	// ---------------新版出口函数要做的事情-------------
	// 1.注销字符设备
	// 2.注销设备号
	// 3.注销设备节点
	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

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

Guess you like

Origin blog.csdn.net/zzy980511/article/details/131506030