10 New character device driver files

1. New character device driver principle

  Because the register_chrdev and unregister_chrdev functions are old version driver files, you can now use the new character device driver API functions.

1. Allocate and release device numbers

  When using the register_chrdev function to register a character device, you only need to give a major device number, but this will cause two problems: 

1. We need to determine in advance which major device numbers are not in use;

2. All minor device numbers under the major device number will be used. For example, if the LED major device number is set to 200, then all the minor device numbers in the range of 0~1048575 will be assigned to one LED device. An LED device must have only one major device number and one minor device number.​ 

  The best way to solve these two problems is to apply to the Linux kernel when using device numbers. Apply for as many as you need. If no device number is specified, use the following function to apply for a device number: 

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

  If the major device number and minor device number of the device are given, just use the following function to register the device number: 

/*
 * @param - from : 要申请的起始设备号(自己给定的设备号)
 * @param - count : 申请的数量
 * @param - name : 设备名字
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name);

  Whether it is through the alloc_chrdev_region function or the register_chrdev_region function, the following release functions are uniformly used:

void unregister_chrdev_region(dev_t from, unsigned count);

  Using the new character device driver, device number allocation example:

int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */

/* 设备号分配 */
if (major) 
{
    /* 定义了主设备号 */
    devid = MKDEV(major, 0);    // 如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0,并且大部分驱动次设备号都选择 0
    register_chrdev_region(devid, 1, "test");
} 
else 
{
    /* 没有定义设备号 */
    alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
    major = MAJOR(devid); /* 获取分配号的主设备号 */
    minor = MINOR(devid); /* 获取分配号的次设备号 */
}

/* 设备号释放 */
unregister_chrdev_region(devid, 1);     /* 注销设备号 */

2. New character device registration method

①Character device structure

  Before writing a character device driver, you need to define a cdev structure variable. This variable represents a character device:

struct cdev test_cdev;

② Initialize character device

  After defining the cdev variable, you must use the cdev_init function to initialize it:

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

  The initialization example is as follows:

struct cdev testcdev;

/* 设备操作函数 */
static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    /* 其他具体的初始项 */
};

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */

③ Add character device

  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 the character device to the Linux system.​ 

/*
 * @param - p : 指向要添加的字符设备(cdev结构体变量)
 * @param - dev : 设备所使用的设备号
 * @param - count : 要添加的设备数量
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

  Add cdev_add Example as follows:

struct cdev testcdev;

/* 设备操作函数 */
static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    /* 其他具体的初始项 */
};

testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops);  /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1);     /* 添加字符设备 */

  A large number of character device drivers in the Linux kernel use this method to add character devices to the Linux kernel.​ 

④ Uninstall character device

  The cdev_del function deletes the corresponding character device from the Linux kernel:

void cdev_del(struct cdev *p);

  An example of deleting a character device is as follows:

cdev_del(&testcdev); /* 删除 cdev */

2. Automatically create device nodes

  If you use modprobe to load the driver module successfully, the corresponding device file will be automatically created in the /dev directory.​ 

1. mdev 

  Use mdev to realize automatic creation and deletion of device node files. buildroot has already taken care of mdev for us.

2. Create and delete classes

  The work of automatically creating device nodes is completed in the entry function of the driver. Generally, the code related to automatically creating device nodes is added after the cdev_add function.​ 

/*
 * @description : 创建的类
 * @param - owner : 一般为 THIS_MODULE
 * @param - name : 设备名字
 * @return : 指向结构体 class 的指针,也就是创建的类
 */
struct class *class_create (struct module *owner, const char *name);


/* 类删除函数 */
void class_destroy(struct class *cls);    // cls 就是要删除的类

3. Create device

  Automatically creating a device node also requires creating a device under this class. Use the device_create function to create a device below the class:

/*
 * @param - cls : 设备要创建哪个类下面
 * @param - parent : 父设备,一般为 NULL
 * @param - devt : 设备号
 * @param - drvdata : 设备可能会使用的一些数据,一般为 NULL
 * @param - fmt : 设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件
 * @return : 创建好的设备
 */
struct device *device_create(struct class *cls,
                             struct device *parent,
                             dev_t devt,
                             void *drvdata,
                             const char *fmt, ...)
    
/* 删除创建的设备 */
void device_destroy(struct class *cls, dev_t devt);  
// classs 是要删除的设备所处的类
// devt 是要删除的设备号

4. Example

struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 创建类 */
    class = class_create(THIS_MODULE, "xxx");
    /* 创建设备 */
    device = device_create(class, NULL, devid, NULL, "xxx");
    return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 删除设备 */
    device_destroy(newchrled.class, newchrled.devid);
    /* 删除类 */
    class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);

3. Set file private data

  Each hardware device has some attributes, such as major device number (dev_t), class, device, switch state (state), etc. When writing the driver, you can write all these attributes in the form of variables. , but for all attribute information of a device, we'd better make it into a structure. When writing the driver open function, add the device structure as private data to the device file:

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

struct test_dev testdev;

/* open 函数 */
static int test_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &testdev; /* 设置私有数据 */
    return 0;
}

  After setting the private data in the open function, read the private_data directly in the write, read, close and other functions to get the device structure.​ 

4. Driver writing

1. Writing LED driver

  Create the 3_newchrled subdirectory under 3/linux/atk-mpl/Drivers, name the VScode workspace newchrled, and create a new file newchrled.c.

#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>

#define NEWCHRLED_CNT			1		  	/* 设备号个数 */
#define NEWCHRLED_NAME			"newchrled"	/* 名字 */
#define LEDOFF 					0			/* 关灯 */
#define LEDON 					1			/* 开灯 */

/* 寄存器物理地址 */
#define PERIPH_BASE     		     	(0x40000000)
#define MPU_AHB4_PERIPH_BASE			(PERIPH_BASE + 0x10000000)
#define RCC_BASE        		    	(MPU_AHB4_PERIPH_BASE + 0x0000)	
#define RCC_MP_AHB4ENSETR				(RCC_BASE + 0XA28)
#define GPIOI_BASE						(MPU_AHB4_PERIPH_BASE + 0xA000)	
#define GPIOI_MODER      			    (GPIOI_BASE + 0x0000)	
#define GPIOI_OTYPER      			    (GPIOI_BASE + 0x0004)	
#define GPIOI_OSPEEDR      			    (GPIOI_BASE + 0x0008)	
#define GPIOI_PUPDR      			    (GPIOI_BASE + 0x000C)	
#define GPIOI_BSRR      			    (GPIOI_BASE + 0x0018)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *MPU_AHB4_PERIPH_RCC_PI;
static void __iomem *GPIOI_MODER_PI;
static void __iomem *GPIOI_OTYPER_PI;
static void __iomem *GPIOI_OSPEEDR_PI;
static void __iomem *GPIOI_PUPDR_PI;
static void __iomem *GPIOI_BSRR_PI;

/* 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		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) 
    {
		val = readl(GPIOI_BSRR_PI);
		val |= (1 << 16);	
		writel(val, GPIOI_BSRR_PI);
	}
    else if(sta == LEDOFF) 
    {
		val = readl(GPIOI_BSRR_PI);
		val|= (1 << 0);	
		writel(val, GPIOI_BSRR_PI);
	}	
}

/*
 * @description		: 取消映射
 * @return 			: 无
 */
void led_unmap(void)
{
	/* 取消映射 */
	iounmap(MPU_AHB4_PERIPH_RCC_PI);
	iounmap(GPIOI_MODER_PI);
	iounmap(GPIOI_OTYPER_PI);
	iounmap(GPIOI_OSPEEDR_PI);
	iounmap(GPIOI_PUPDR_PI);
	iounmap(GPIOI_BSRR_PI);
}

/*
 * @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)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) 
    {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) 
    {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) 
    {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	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)
{
	u32 val = 0;
	int ret;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
    MPU_AHB4_PERIPH_RCC_PI = ioremap(RCC_MP_AHB4ENSETR, 4);
    GPIOI_MODER_PI = ioremap(GPIOI_MODER, 4);
    GPIOI_OTYPER_PI = ioremap(GPIOI_OTYPER, 4);
    GPIOI_OSPEEDR_PI = ioremap(GPIOI_OSPEEDR, 4);
    GPIOI_PUPDR_PI = ioremap(GPIOI_PUPDR, 4);
    GPIOI_BSRR_PI = ioremap(GPIOI_BSRR, 4);

    /* 2、使能PI时钟 */
    val = readl(MPU_AHB4_PERIPH_RCC_PI);
    val &= ~(0X1 << 8); /* 清除以前的设置 */
    val |= (0X1 << 8);  /* 设置新值 */
    writel(val, MPU_AHB4_PERIPH_RCC_PI);

    /* 3、设置PI0通用的输出模式。*/
    val = readl(GPIOI_MODER_PI);
    val &= ~(0X3 << 0); /* bit0:1清零 */
    val |= (0X1 << 0);  /* bit0:1设置01 */
    writel(val, GPIOI_MODER_PI);

    /* 3、设置PI0为推挽模式。*/
    val = readl(GPIOI_OTYPER_PI);
    val &= ~(0X1 << 0); 
    writel(val, GPIOI_OTYPER_PI);

    /* 4、设置PI0为高速。*/
    val = readl(GPIOI_OSPEEDR_PI);
    val &= ~(0X3 << 0); /* bit0:1 清零 */
    val |= (0x2 << 0); /* bit0:1 设置为10*/
    writel(val, GPIOI_OSPEEDR_PI);

    /* 5、设置PI0为上拉。*/
    val = readl(GPIOI_PUPDR_PI);
    val &= ~(0X3 << 0); /* bit0:1 清零*/
    val |= (0x1 << 0); /*bit0:1 设置为01*/
    writel(val,GPIOI_PUPDR_PI);

    /* 6、默认关闭LED */
    val = readl(GPIOI_BSRR_PI);
    val |= (0x1 << 0);
    writel(val, GPIOI_BSRR_PI);


	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) 
    {		
        /* 定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
		if(ret < 0) 
        {
			pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRLED_NAME, NEWCHRLED_CNT);
			goto fail_map;
		}
	} 
    else 
    {						
        /* 没有定义设备号 */
		ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		if(ret < 0) 
        {
			pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRLED_NAME, ret);
			goto fail_map;
		}
		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 */
	ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
	if(ret < 0)
    {
        goto del_unregister;
    }

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

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

destroy_class:
	class_destroy(newchrled.class);
del_cdev:
	cdev_del(&newchrled.cdev);
del_unregister:
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);
fail_map:
	led_unmap();
	return -EIO;

}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 取消映射 */
   led_unmap();
   
	/* 注销字符设备驱动 */
	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("LXS");
MODULE_INFO(intree, "Y");

  Here is a flow chart for easy understanding:

2. Write test APP 

  This is tested using the ledApp.c from last time.

5. Run the test

1. Compile the driver

  Write Makefile:

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linux内核源码路径
CURRENT_PATH := $(shell pwd)		# 获取当前所处路径
obj-m := newchrled.o		

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  input the command:

make
# 成功后会出现一个 newchrled.ko 驱动模块文件

2. Compile and test APP

  Use a cross-compiler to compile the APP test program:

arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

3. Run the test

  You need to copy ledApp newchrled.ko to the /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ directory.

depmod              # 第一次加载驱动的时候需要运行此命令
modprobe newchrled  # 加载驱动

  Apply for a major device number of 241 and a minor device number of 0. And the driver will automatically create the device node file /dev/newchrdev in the /dev directory:

  Test again whether the red LED light can work:

./ledApp /dev/newchrled 1  # 打开 LED 灯

./ledApp /dev/newchrled 0  # 关闭 LED 灯

# 最后卸载驱动
rmmod newchrled

Summary: Compared with the previous chapter, there are more automatic device assignments, new character device registration methods, automatic creation of device nodes and setting file private data. The rest is not much different from the previous chapter.

Guess you like

Origin blog.csdn.net/qq_45475497/article/details/135037489