# 字符设备驱动模型 ## 字符设备驱动注册的步骤 1. 自己创建`struct file_operations`结构体fp 2. 在入口函数,申请设备号,把fp占着该设备号对应的槽 3. 在入口函数,创建类`struct class` 4. 在入口函数,在类下创建设备节点 5. 在出口函数,倒着来, - 删除设备节点 - 删除类 - 释放fp所占的槽 ## 基本概念 ### 设备号 #### 设备号组成 Linux提供`dev_t`的数据类型表示设备号:高12位主设备号(大小:0-4095),低20位位次设备号 ```c // include/linux/types.h typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t; // 所以可以看出dev_t就是一个uint32_t 的数据 ``` #### 设备号常用函数 ```c //include/linux/kdev_t.h #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 从设备号中得到主设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 从设备号中得到次设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 把主设备号和次设备号组装成dev_t类型的linux设备号 ``` ### 字符设备 #### linux用cdev表示一个字符设备 ```c struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; // 操作函数集合 struct list_head list; dev_t dev; // 设备id unsigned int count; }; ``` #### 入口函数中定义字符设备步骤如下 ```c struct cdev testcdev; // 定义字符设备结构体 testcdev.owner = THIS_MODULE; cdev_init(&testcdev, &test_fops); // 字符设备结构体 初始化 cdev_add(&testcdev, devid, 1); // 把字符设备添加到linux内核中 ``` #### 出口函数删除字符设备 ```c cdev_del(&testcdev); ``` #### 函数原型介绍 ```c /** * *@cdev:字符设备 *@fops:file_operations结构体指针 */ void cdev_init(struct cdev *cdev, const struct file_operations *fops) /** *用于向 Linux系统添加字符设备 (cdev结构体变量 ) *@p:指向要往系统添加的字符设备 *@count:要添加的设备数量 */ int cdev_add(struct cdev *p, dev_t dev, unsigned count) /** *驱动卸载的时候要从linux内核中卸载字符设备 *@p:指向要往系统添加的字符设备 */ void cdev_del(struct cdev *p) ``` 在 2.4 的内核我们使用 `major = register_chrdev(0, "xym_led", &led_drv)` 来进行字符设备注册,在注册过程中分配了设备号,这种方式每一个主设备号只能存放一种设备,它们使用相同的 `file_operation` 结构体,也就是说内核最多支持 256 个字符设备驱动程序。 在 2.6 的内核之后,新增了一个 `register_chrdev_region` 函数,它支持将同一个主设备号下的次设备号进行分段,每一段供给一个字符设备驱动程序使用,使得资源利用率大大提升,同时,2.6 的内核以后保留了原有`register_chrdev` 方法。 - 老版本 内核在入口函数都是使用`register_chrdev`函数来确定主设备号: ```c major = register_chrdev(0, "xym_led", &led_drv); // 第一个参数是0的话表示自动申请设备号 ``` 函数调用关系图: ``` register_chrdev -》__register_chrdev(major, 0, 256, name, fops) -》__register_chrdev_region(major, baseminor, count, name) -》cdev = cdev_alloc(); -》cdev_add ``` 从上面的调用关系可以看出,`register_chrdev`完成2部分工作 1. 调用`__register_chrdev_region`完成设备号申请 2. 完成了字符设备cdev的创建和添加到内核工作。 **上面用到的函数原型如下**: ```c /** * 设备号申请函数 *@major :主设备号,如果为0,那么系统默认会自动获取主设备号返回 *@name:设备名字 *@fops: 驱动的 file_operations函数集合 */ //include/linux/fs.h static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { /* * 这个地方就是我们说的为什么使用register_chrdev函数获取设备号的时候次设备号一下子就没了, * 这里从0开始,申请了256个次设备号,就是一个主设备号major号下的256个次设备号(0-255)全用完了 */ return __register_chrdev(major, 0, 256, name, fops); } int __register_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name, const struct file_operations *fops) { struct char_device_struct *cd; struct cdev *cdev; cd = __register_chrdev_region(major, baseminor, count, name);// 申请设备号 cdev = cdev_alloc(); // 创建cdev cdev->owner = fops->owner; cdev->ops = fops; kobject_set_name(&cdev->kobj, "%s", name); err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); // 增加cdev到内核 cd->cdev = cdev; return major ? 0 : cd->major; // ………………… } /** * 设备号释放函数 *@major :主设备号,如果为0,那么系统默认会自动获取主设备号返回 *@name:设备名字 */ static inline void unregister_chrdev(unsigned int major, const char *name) ``` - 新版本 在入口函数中使用`register_chrdev_region`或者`alloc_chrdev_region`函数获取主设备号: ``` if(major){ devid = MKDEV(major, 0); register_chrdev_region(devid, 1, "xym_led"); /* 设备号事先确定的情况 */ -》__register_chrdev_region(MAJOR(n), MINOR(n),next - n, name); }else{ alloc_chrdev_region(&devid, 0, 1, "xym_led"); /* 设备号事先不确定,申请设备号 */ -》__register_chrdev_region(0, baseminor, count, name); major = MAJOR(devid); /* 获取主设备号 */ minor = MINOR(devid); /* 获取次设备号 */ } /* 这里 需要手动创建字符设备,并添加到内核 */ struct cdev cdev; cdev.owner = THIS_MODULE; cdev_init(&cdev, &led_drv); cdev_add(&cdev, devid, 1); ``` 从上面的调用关系可以看出,`register_chrdev_region`或者`alloc_chrdev_region`完成1部分工作 1. 调用`__register_chrdev_region`完成设备号申请,和老版本一样; **上面用到的函数原型如下**: ```c /** * 需要在驱动的入口函数申请设备号 如果设备号已经确定,那么使用 register_chrdev_region 函数 申请设备id *@from :要从哪个设备id开始申请设备号 *@count :要申请的设备号数量。 *@name :设备名字 */ int register_chrdev_region(dev_t from, unsigned count, const char *name); /** * 需要在驱动的入口函数申请设备号 如果主设备号没有确定 使用alloc_chrdev_region函数申请设备id *@dev :保存申请到的设备号 *@baseminor :次设备号起始地址,可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor为起始地址地址开始递增。一般从0,也就是说次设备号从0开始。 *@count :要申请的设备号数量。 *@name :设备名字 */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) /** * 设备号释放函数,在出口函数中调用,字符设备释放函数,无论是register_chrdev_region或者alloc_chrdev_region注册的字符设备,都用该函数进行释放 *@from :要释放的设备号。 *@count:表示从 from开始,要释放的设备号数量 */ void unregister_chrdev_region(dev_t from, unsigned count) ``` **总结**:从调用关系可以看出无论是新版本还是老版本,获取设备号的方式,最总都是 `__register_chrdev_region`函数实现,只是老版本`register_chrdev`不仅仅会分配设备号,还自动创建了并向内核添加了字符设备cdev,而新版本需要用户手动完成 cdev的创建和添加到内核。 **注意**:使用`cat /proc/devices` 命令查看当前系统已经使用的了设备号 ### 设备节点 #### 命令行手动创建设备节点 ```bash /dev/chrdevbase :设备节点名字 c :代表字符设备驱动 200 : 主设备号 0 : 次设备号 mknod /dev/chrdevbase c 200 0 ``` #### 自动创建设备节点 在驱动入口函数中先创建类,然后在类下创建设备 ```c /* 创建类,类名 xym_led_class*/ led_class = class_create(THIS_MODULE, "xym_led_class"); /* 类下创建设备,那么 /dev/xym_led 即是显示的设备节点*/ device_create(led_class, NULL, devid, NULL, "xym_led"); ``` 在出口函数就要设备类和设备节点 ```c device_destroy(led_class, devid); // 摧毁类下的设备 class_destroy(led_class); // 摧毁类 ``` #### 函数原型 ##### 创建类和删除类 自动创建设备节点相关代码。首先要创建一个 class类, class是个结构体,定义在文件 include/linux/device.h里面。 class_create是类创建函数, class_create是个宏定义,内容如下: ```c /** * *@owner:THIS_MODULE *@name:类名 */ #define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ }) struct class *__class_create(struct module *owner, const char *name, 8 struct lock_class_key *key) /** * *@cls:指向要卸载的类指针 */ void class_destroy(struct class *cls); ``` ##### 创建设备和删除设备 ```c /** * *@class:指向要在哪个类下面创建设备的类指针 *@parent:父亲,一般为NULL *@devt : 设备id *@drvdata:设备可能会使用的一些数据 *@fmt :可变参数,设备名字,该名字会在/dev/目录下显示 */ /* * 建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。 * 使用 device_create函数在类下面创建设备, device_create函 数原型如下: */ struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) ``` 在设备卸载的时候,即出口函数中调用摧毁函数,参数 classs是要删除的设备所处的类,参数 devt是要删除的设备号。 ```c /** * *@class:指向要删除的设备所处的类 *@devt : 设备id */ void device_destroy(struct class *class, dev_t devt) ``` ### mmu地址映射 ![](media/image-20200601095304812.png) - 完成虚拟空间到物理空间的映射。 - 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。 #### ioremap函数 Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。当我们想访问物理地址的时候就必须做相应的转换`ioremap`函数。 `ioremap`函数用于获取指定物理地址空间对应的虚拟地址空间,定义在`arch/arm/include/asm/io.h`文件中,定义如下: ```c /** *@phys_addr :要映射给的物理起始地址。 *@*@size :要映射的内存空间大小。 mtype ioremap的类型,可以选择 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED和 *@MT_DEVICE_WC ioremap函数选择 MT_DEVICE。 *返回值 : __iomem类型的指针,指向映射后的虚拟空间首地址。 */ #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)); } ``` 例如我们要访问GPIO1_IO03的寄存器,可以这样定义 ```c #define SW_MUX_GPIO1_IO03_BASE (0X020E0068) // 查找手册确定要操作的寄存器物理地址 static void __iomem* SW_MUX_GPIO1_IO03; SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);// 通过ioremap把物理地址转换为虚拟地址 SW_MUX_GPIO1_IO03 // 这样就可以用SW_MUX_GPIO1_IO03访问了 ``` #### iounmap函数 卸载驱动的时候需要使用 iounmap函数释放掉 ioremap函数所做的映射, iounmap函数原型如下: ``` 函数原型 void iounmap (volatile void __iomem *addr) iounmap(SW_MUX_GPIO1_IO03); ``` #### 内核空间的内存操作函数 使用 ioremap函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。 ##### 读操作函数 ``` u8 readb(const volatile void __iomem *addr) u16 readw(const volatile void __iomem *addr) u32 readl(const volatile void __iomem *addr) ``` readb、 readw和 readl这三个函数分别对应 8bit、 16bit和 32bit读操作,参数 addr就是要读取写内存地址,返回值就是读取到的数据。 ##### 写操作函数 写操作函数有如下几个: ``` void writeb(u8 value, volatile void __iomem *addr) void writew(u16 value, volatile void __iomem *addr) void writel(u32 value, volatile void __iomem *addr) ``` writeb、 writew和 writel这三个函数分别对应 8bit、 16bit和 32bit写操作,参数 value是要写入的数值, addr是要写入的地址。 ## printf 和printk printf运行在用户空间,printk运行在内核空间,可以根据日志级别对消息进行分类,一共有 8个消息级别,这 8个消息级别定义在文件 include/linux/kern_levels.h里面,定 义如下: ```c #define KERN_SOH "\001" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */ #define KERN_ALERT KERN_SOH "1" /* 必 须立即采取行动 */ #define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */ #define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR报告硬件错误 */ #define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */ #define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */ #define KERN_INFO KERN_SOH "6" /* 提示性的信息 */ #define KERN_DEBUG KERN_SOH "7" /* 调试信息 */ 一共定义了 8个级别,其中 0的优先级最高, 7的优先级最低。 ``` 如果要设置消息级别,参考如下示例: ``` printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); ``` 不显式的设置消息级别,那么 printk将会采用默认级别 ```bash MESSAGE_LOGLEVEL_DEFAULT默认为 4。 在 include/linux/printk.h中有个宏 #define CONSOLE_LOGLEVEL_DEFAULT 7 ``` 消息基本高于7的才会被显示出来 ## 驱动模块常用命令 1. insmod xxx.ko:装载驱动 2. cat /proc/device:查看 3. lsmod:查看内核中已经加载的驱动模块 4. rmod xxx:卸载驱动 5. dmesg:驱动里面printk的打印信息在加载驱动的时候不一定会显示,可以使用该命令查看 ## 代码示例 ### 老版本参见【01.led_drv】工程 前面介绍了一个简单的led驱动框架,下面就按照该框架进行程序填充: 1. 自己创建`struct file_operations`结构体fp ```c /* * 1. 定义自己的file_operations结构体 */ static struct file_operations led_file_op = { .owner = THIS_MODULE, .open = led_drv_open, .read = led_drv_read, .write = led_drv_write, .release = led_drv_close, }; ``` 2. 在入口函数,申请设备号,把fp占着该设备号对应的槽 3. 在入口函数,创建类`struct class` 4. 在入口函数,在类下创建设备节点 ```c /** * @description: led_init - 入口函数 */ static int __init led_init(void) { /* * 2: 自动获取主设备号 */ g_led_dev.major = register_chrdev(0, "xym_led", &led_file_op); /* 注册字符设备驱动 */ /* * 3: 创建类 */ g_led_dev.class = class_create(THIS_MODULE, "xym_led_class"); if (IS_ERR(g_led_dev.class)) { unregister_chrdev(g_led_dev.major, "xym_led"); goto led_init_error; } /* * 4.在类下创建设备节点设备 /dev/xym_led */ g_led_dev.devid = MKDEV(g_led_dev.major,0); device_create(g_led_dev.class, NULL, g_led_dev.devid, NULL, "xym_led"); /* /dev/xym_led */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return 0; led_init_error: printk("%s %s line %d:init error !!!\n", __FILE__, __FUNCTION__, __LINE__); return -1; } ``` 5. 在出口函数,倒着来, - 删除设备节点 - 删除类 - 释放fp所占的槽 ```c static void __exit led_exit(void) { /* * 5.倒着来 释放资源 */ hw_led_reinit(); device_destroy(g_led_dev.class, g_led_dev.devid); /* 删除设备节点 */ class_destroy(g_led_dev.class); /* 删除类 */ unregister_chrdev(g_led_dev.major, "xym_led"); /* 删除字符设备槽 */ printk("%s %s line %d: rmmod ! \n", __FILE__, __FUNCTION__, __LINE__); } ``` ### 新版本参见【02.led_driver_new】工程 前面介绍了一个简单的led驱动框架,下面就按照该框架进行程序填充: 1. 自己创建`struct file_operations`结构体fp ```c /* * 1. 定义自己的file_operations结构体 */ static struct file_operations led_drv = { .owner = THIS_MODULE, .open = led_drv_open, .read = led_drv_read, .write = led_drv_write, .release = led_drv_close, }; ``` 2. 在入口函数,申请设备号,把fp占着该设备号对应的槽 3. 在入口函数,创建类`struct class` 4. 在入口函数,在类下创建设备节点 ```c static int __init led_init(void) { /* * 2: 自动获取主设备号 */ if(g_led_dev.major){ g_led_dev.devid = MKDEV(g_led_dev.major, 0); register_chrdev_region(g_led_dev.devid, 1, "xym_led"); }else{ alloc_chrdev_region(&g_led_dev.devid, 0, 1, "xym_led"); /* 申请设备号 */ g_led_dev.major = MAJOR(g_led_dev.devid); /* 获取主设备号 */ g_led_dev.minor = MINOR(g_led_dev.devid); /* 获取次设备号 */ } printk("newcheled major=%d,minor=%d\r\n",g_led_dev.major, g_led_dev.minor); /* * 2.1:初始化cdev */ g_led_dev.cdev.owner = THIS_MODULE; cdev_init(&g_led_dev.cdev, &led_drv); /* * 2.3:添加cdev 到 内核 */ cdev_add(&g_led_dev.cdev, g_led_dev.devid, 1); /* * 3: 创建类 */ g_led_dev.class = class_create(THIS_MODULE, "xym_led_class"); /* * 4.在类下创建设备节点设备 /dev/xym_led */ device_create(g_led_dev.class, NULL, g_led_dev.devid, NULL, "xym_led"); /* /dev/xym_led */ printk("%s %s line %d:insmod !\n", __FILE__, __FUNCTION__, __LINE__); return 0; led_init_error: printk("%s %s line %d:init error !!!\n", __FILE__, __FUNCTION__, __LINE__); return -1; } ``` 5. 在出口函数,倒着来, - 删除设备节点 - 删除类 - 释放fp所占的槽 ```c static void __exit led_exit(void) { /* * 5.倒着来 释放资源 */ hw_led_reinit(); cdev_del(&g_led_dev.cdev); unregister_chrdev_region(g_led_dev.devid, 1); /* 卸载掉1个 */ device_destroy(g_led_dev.class, g_led_dev.devid); class_destroy(g_led_dev.class); printk("%s %s line %d: rmmod ! \n", __FILE__, __FUNCTION__, __LINE__); } ``` ## 实验步骤 1. 工程编译完成后会生成两个文件 - led_app - xym_led.ko 2. 把两个文件拷贝到nfs文件系统运行 - insmod xym_led.ko :加载驱动模块 - lsmod:查看内核中已经加载的驱动模块 - ./led_app on 或者./led_app off 打开或者关闭led - rmod xym_led:卸载驱动模块 - lsmod:查看内核中已经加载的驱动模块