tag 标签: 字符设备驱动

相关博文
  • 热度 4
    2023-11-30 08:32
    1127 次阅读|
    0 个评论
    一文秒懂|Linux字符设备驱动 1、前言 众所周知, Linux 内核主要包括三种驱动模型,字符设备驱动,块设备驱动以及网络设备驱动。 其中, Linux 字符设备驱动,可以说是 Linux 驱动开发中最常见的一种驱动模型。 我们该系列文章,主要为了帮助大家快速入门 Linux 驱动开发,该篇主要来了解一些字符设备驱动的框架和机制。 系列文章基于 Kernel 4.19 2、关键数据结构 2.1 cdev struct cdev { struct kobject kobj ; struct module * owner ; const struct file_operations * ops ; struct list_head list ; dev_t dev ; unsigned int count ; } __randomize_layout ; 结构体名称 : cdev 文件位置 : include/linux/cdev.h 主要作用 : cdev 可以理解为 char device ,用来抽象一个字符设备。 核心成员及含义 : kobj :表示一个内核对象。 owner :指向该模块的指针 ops :指向文件操作的指针,包括 open 、 read 、 write 等操作接口 list :用于将该设备加入到内核模块链表中 dev :设备号,由主设备号和次设备号构成 count :表示有多少个同类型设备,也间接表示设备号的范围 __randomize_layout :一个编译器指令,用于随机化结构体的布局,以增加安全性。 2.2 file _ operations struct file_operations { struct module * owner ; loff_t ( * llseek ) ( struct file * , loff_t , int ); ssize_t ( * read ) ( struct file * , char __user * , size_t , loff_t * ); ssize_t ( * write ) ( struct file * , const char __user * , size_t , loff_t * ); ssize_t ( * read_iter ) ( struct kiocb * , struct iov_iter * ); ssize_t ( * write_iter ) ( struct kiocb * , struct iov_iter * ); int ( * iterate ) ( struct file * , struct dir_context * ); int ( * iterate_shared ) ( struct file * , struct dir_context * ); __poll_t ( * poll ) ( struct file * , struct poll_table_struct * ); long ( * unlocked_ioctl ) ( struct file * , unsigned int , unsigned long ); long ( * compat_ioctl ) ( struct file * , unsigned int , unsigned long ); int ( * mmap ) ( struct file * , struct vm_area_struct * ); unsigned long mmap_supported_flags ; int ( * open ) ( struct inode * , struct file * ); int ( * flush ) ( struct file * , fl_owner_t id ); int ( * release ) ( struct inode * , struct file * ); int ( * fsync ) ( struct file * , loff_t , loff_t , int datasync ); int ( * fasync ) ( int , struct file * , int ); int ( * lock ) ( struct file * , int , struct file_lock * ); ssize_t ( * sendpage ) ( struct file * , struct page * , int , size_t , loff_t * , int ); unsigned long ( * get_unmapped_area )( struct file * , unsigned long , unsigned long , unsigned long , unsigned long ); int ( * check_flags )( int ); int ( * flock ) ( struct file * , int , struct file_lock * ); ssize_t ( * splice_write )( struct pipe_inode_info * , struct file * , loff_t * , size_t , unsigned int ); ssize_t ( * splice_read )( struct file * , loff_t * , struct pipe_inode_info * , size_t , unsigned int ); int ( * setlease )( struct file * , long , struct file_lock ** , void ** ); long ( * fallocate )( struct file * file , int mode , loff_t offset , loff_t len ); void ( * show_fdinfo )( struct seq_file * m , struct file * f ); #ifndef CONFIG_MMU unsigned ( * mmap_capabilities )( struct file * ); #endif ssize_t ( * copy_file_range )( struct file * , loff_t , struct file * , loff_t , size_t , unsigned int ); int ( * clone_file_range )( struct file * , loff_t , struct file * , loff_t , u64 ); int ( * dedupe_file_range )( struct file * , loff_t , struct file * , loff_t , u64 ); int ( * fadvise )( struct file * , loff_t , loff_t , int ); } __randomize_layout ; ​ 结构体名称 : file_operations 文件位置 : include/linux/fs.h 主要作用 :正如其名,主要用来描述文件操作的各种接口, Linux 一切接文件的思想,内核想要操作哪个文件,都需要通过这些接口来实现。 核心成员及含义 : open :打开文件的函数 read :读取文件的函数。 write :写入文件的函数。 release :关闭文件的函数。 flush :刷新文件的函数,通常在关闭文件时调用。 llseek :改变文件读写指针位置的函数。 fsync :将文件数据同步写入磁盘的函数。 poll :询问文件是否可被非阻塞读写 2.3 dev _ t typedef u32 __kernel_dev_t ; ​ typedef __kernel_dev_t dev_t ; 类型名称 : dev_t 文件位置 : include/linux/types.h 主要作用 :表示字符设备对应的设备号,其中包括主设备号和次设备号。 3、数据结构之间关系 上图绘制是对字符设备驱动程序的数据结构以及 API 的关系图, 有需要原始文件,可在公~号【嵌入式艺术】获取。 4、字符设备驱动整体架构 4.1 加载与卸载函数 驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。 我们一般这么定义驱动的加载卸载函数: static int __init xxx_init ( void ) { ​ } ​ static void __exit xxx_exit ( void ) { } ​ module_init ( xxx_init ); module_exit ( xxx_exit ); 这段代码就是实现一个通用驱动的加载与卸载,关于 module_init 和 module_exit 的实现机制,可以查看之前总结文章。 4.2 设备号管理 4.2.1 设备号的概念 每一类字符设备都有一个唯一的设备号,其中设备号又分为主设备号和次设备号,那么这两个分别作用是什么呢? 主设备号:用于标识设备的类型, 次设备号:用于区分同类型的不同设备 简单来说,主设备号用于区分是 IIC 设备还是 SPI 设备,而次设备号用于区分 IIC 设备下,具体哪一个设备,是 MPU6050 还是 EEPROM 。 4.2.2 设备号的分配 了解了设备号的概念, Linux 中设备号有那么多,那么我们该如何去使用正确的设备号呢? 设备号的分配方式有两种,一种是动态分配,另一种是静态分配,也可以理解为一种是内核自动分配,一种是手动分配。 静态分配函数 : int register_chrdev_region ( dev_t from , unsigned count , const char * name ); from :表示已知的一个设备号 count :表示连续设备编号的个数,(同类型的设备有多少个) name :表示设备或者驱动的名称 函数作用 :以 from 设备号开始,连续分配 count 个同类型的设备号 动态分配函数 : int alloc_chrdev_region ( dev_t * dev , unsigned baseminor , unsigned count , const char * name ); dev :设备号的指针,用于存放分配的设备号的值 baseminor :次设备号开始分配的起始值 count :表示连续设备编号的个数,(同类型的设备有多少个) name :表示设备或者驱动的名称 函数作用 :从 baseminor 次设备号开始,连续分配 count 个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给 *dev 这两个函数最大的区别在于 : register_chrdev_region :调用前,已预先定义好了主设备号和次设备号,调用该接口后,会将自定义的设备号登记加入子系统中,方便系统追踪系统设备号的使用情况。 alloc_chrdev_region :调用前,未定义主设备号和次设备号;调用后,主设备号以 0 来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况。 这两个函数的共同点在于 : 系统维护了一个数组列表,用来登记所有的已使用的设备号信息,这两个接口归根到底也是将其设备号信息,登记到系统维护的设备号列表中,以免后续冲突使用。 在 Linux 中,我们可以通过 cat /proc/devices 命令,查看所有i登记的设备号列表。 后面有时间,我们可以详细聊设备号的自动分配机制,管理机制。 4.2.3 设备号的注销 设备号作为一种系统资源,当所对应的设备卸载时,当然也要将其所占用的设备号归还给系统,无论时静态分配,还是动态分配,最终都是调用下面函数来注销的。 void unregister_chrdev_region ( dev_t from , unsigned count ); from :表示已知的一个设备号 count :表示连续设备编号的个数,(同类型的设备有多少个) 函数作用 :要注销 from 主设备号下的连续 count 个设备 4.2.4 设备号的获取 设备号的管理很简单,在关键数据结构中,我们看到设备号的类型是 dev_t ,也就是 u32 类型表示的一个数值。 其中主设备号和次设备号的分界线,由 MINORBITS 宏定义指定: #define MINORBITS 20 也就是主设备号占用高 12bit ,次设备号占用低 20bit 并且,内核还提供了相关 API 接口,来获取主设备号和次设备号,以及生成设备号的接口,如下: #define MINORMASK ((1U << MINORBITS) - 1) ​ #define MAJOR(dev) MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 以上,通过移位操作,来实现主次设备号的获取。 4.2.4 通用代码实现 #define CUSTOM_DEVICE_NUM 0 #define DEVICE_NUM 1 #device DEVICE_NAME "XXXXXX" static dev_t global_custom_major = CUSTOM_DEVICE_NUM ; ​ static int __init xxx_init ( void ) { dev_t custom_device_number = MKDEV ( global_custom_major , 0 ); // custom device number /* device number register*/ if ( global_custom_major ) { ret = register_chrdev_region ( custom_device_number , DEVICE_NUM , DEVICE_NAME ); } else { ret = alloc_chrdev_region ( & custom_device_number , 0 , DEVICE_NUM , DEVICE_NAME ); global_custom_major = MAJOR ( custom_device_number ); } } ​ static void __exit xxx_exit ( void ) { unregister_chrdev_region ( MKDEV ( global_mem_major , 0 ), DEVICE_NUM ); } ​ module_init ( xxx_init ); module_exit ( xxx_exit ); 该函数实现了设备号的分配,如果主设备号为 0 ,则采用动态配分的方式,否则采用静态分配的方式。 更多干货可见: 高级工程师聚集地 ,助力大家更上一层楼! 4.3 字符设备的管理 了解完设备号的管理之后,我们来看下字符设备是如何管理的。 4.3.1、字符设备初始化 void cdev_init ( struct cdev * cdev , const struct file_operations * fops ); cdev :一个字符设备对象,也就是我们创建好的字符设备 fops :该字符设备的文件处理接口 函数作用 :初始化一个字符设备,并且将所对应的文件处理指针与字符设备绑定起来。 4.3.2、字符设备注册 int cdev_add ( struct cdev * p , dev_t dev , unsigned count ); p :一个字符设备指针,只想待添加的字符设备对象 dev :该字符设备所负责的第一个设备编号 count :该类型设备的个数 函数作用 :添加一个字符设备驱动到 Linux 系统中。 4.3.3、字符设备注销 void cdev_del ( struct cdev * p ); p :指向字符设备对象的指针 函数作用 :从系统中移除该字符设备驱动 4.4 文件操作接口的实现 因为在 Linux 中,一切皆文件的思想,所以每一个字符设备,也都有一个文件节点来对应。 我们在初始化字符设备的时候,会将 struct file_operations 的对象与字符设备进行绑定,其作用是来处理该字符设备的 open 、 read 、 write 等操作。 我们要做的就是去实现我们需要的函数接口,如: static const struct file_operations global_mem_fops = { . owner = THIS_MODULE , . llseek = global_mem_llseek , . read = global_mem_read , . write = global_mem_write , . unlocked_ioctl = global_mem_ioctl , . open = global_mem_open , . release = global_mem_release , }; 至此,我们一个基本的字符设备驱动程序的框架就基本了然于胸了 5、总结 本篇文章,旨在通俗易懂的讲解: 字符设备驱动相关数据结构 数据结构关系图 核心 API 接口 字符设备驱动整体框架 希望对大家有所帮助。
  • 热度 14
    2013-7-23 15:11
    1188 次阅读|
    1 个评论
            本章以按键驱动程序的编写为例来说明字符设备驱动编写中如何加入中断功能。继上一章中杂项设备驱动的编写方法,在编写带有中断的字符设备驱动时,需要在open()设备文件时配置设备中断之后申请中断(request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)),其中参数irq在此表示外部中断号;handler在此表示中断处理函数指针;flags在此表示中断触发条件;dev在此表示传递给handler的参数。当close()设备文件时需要释放中断(free_irq(unsigned int irq, void *(参数未知)))。下面将编写好的驱动文件(tiny210_buttons.c)编译成内核模块并动态加载。之后运行buttons可执行程序观察实验现象如图20: 图20 按下按键后现象如图21: 图21 上面的测试程序每间隔5s钟读取一次按键信息,不能及时响应按键,而且按键驱动程序中不带有去抖动功能。为此可以编写更可靠的驱动程序。具体编写思路如下:在发生按键中断时启动一个定时器定时毫秒级别,定时时间到了之后读取按键对应IO口状态以实现软件延时去抖动的功能;为了实时响应按键还可以以阻塞方式读取按键键值信息,一旦发现有按键中断解除阻塞并获取按键键值,为此我们需要在读取设备数据的驱动函数中等待按键事件,在延时去抖动延时到来之后释放等待进程队列。下面将修改后的按键驱动程序(mini210_buttons.c)重新编译并动态加载后运行测试程序观察实验现象如图22: 图22
  • 热度 18
    2013-7-21 21:47
    1086 次阅读|
    0 个评论
    本章以两种方式讲述一下LED驱动的编写方法,并从这两种方法中学习字符设备驱动编写的框架。这两种方法分别是:以杂项设备驱动框架来编写LED驱动和以标准字符设备驱动框架来编写LED驱动。   首先讲述以杂项设备驱动方式来驱动LED。杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的include/linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10 ,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。杂项设备的注册和注销函数分别是misc_register(struct miscdevice  *misc)和misc_deregister(struct miscdevice *misc)。这两个函数分别在注册模块module_init和注销模块module_exit时被调用。杂项设备注册的关键就是实现struct miscdevice结构体的成员函数功能。struct miscdevice结构体定义如下:   struct miscdevice {  int minor;                                                /*次设备号(主设备号为10)*/  const char *name;                                     /*misc设备名*/  const struct file_operations *fops;                     /*misc设备文件操作结构体*/  struct list_head list;  struct device *parent;  struct device *this_device;  const char *nodename;  mode_t mode; };   其中minor一般选择MISC_DYNAMIC_MINOR(自动分配次设备号),name是注册设备时分配的设备名(动态加载该驱动模块后会在/dev/路径中发现以name命名的设备文件,这也是应用程序驱动LED的设备文件名),struct file_operations 结构体包含驱动字符设备的各种操作函数。struct file_operations结构体定义如下:   struct file_operations {        struct module *owner;                                                                  // 模块所有者        loff_t (*llseek) (struct file *, loff_t, int);        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);        int (*readdir) (struct file *, void *, filldir_t);        unsigned int (*poll) (struct file *, struct poll_table_struct *);        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);    // IO控制函数        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);        int (*mmap) (struct file *, struct vm_area_struct *);        int (*open) (struct inode *, struct file *);        int (*flush) (struct file *, fl_owner_t id);        int (*release) (struct inode *, struct file *);        int (*fsync) (struct file *, int datasync);        int (*aio_fsync) (struct kiocb *, int datasync);        int (*fasync) (int, struct file *, int);        int (*lock) (struct file *, int, struct file_lock *);        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);        int (*check_flags)(int);        int (*flock) (struct file *, int, struct file_lock *);        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);        int (*setlease)(struct file *, long, struct file_lock **);        long (*fallocate)(struct file *file, int mode, loff_t offset,                        loff_t len); };   owner是模块所有者,一般定义为THISZ_MODULE。在驱动LED时我们只需实现unlocked_ioctl(struct file *flip, unsigned int cmd, unsigned long arg)(注意各参数的类型和含义,在实现该函数时须明白每个形参的含义)即可。在编写LED的驱动时,这两个结构体分别赋值如下:   static struct file_operations mini210_led_dev_fops = {   .owner = THIS_MODULE,   // “.”加结构体成员的写法的含义是对于未赋值的成员编译器自动为其赋值NULL          .unlocked_ioctl = mini210_leds_ioctl,  }; static struct miscdevice mini210_led_dev = {        .minor = MISC_DYNAMIC_MINOR,        .name = DEVICE_NAME,        .fops       = mini210_led_dev_fops, };   最后,在模块注册时(module_init)时需要对IO口做初始化,在注销模块时(module_exit)需要回复IO口配置。对IO口操作的一些IO接口函数就不一一详解了,如果需要可以查看gpiolib.c源代码(linux-3.0.8/drivers/gpio/gpiolib.c)。          前面讲述了,如何以杂项设备的驱动框架来编写LED驱动,下面讲解如何测试编写好的驱动。   1、按上一章中动态加载内核模块的操作方法,编译出mini210_led.ko(驱动源代码为mini210_led.c)内核模块。   2、对编写好的测试程序进行交叉编译(Makefile已经编写完成)。   3、将mini210_led.ko内核模块和交叉编译生成的可执行程序(led)拷贝到/home/nfs路径中。   4、在tiny210的终端中挂载ubuntu的/home/nfs路径。如图14:   图14   5、动态加载模式加载mini210_led.ko,并查看/dev/路径下是否生成了myleds(编写驱动时为struct miscdevice的name成员赋的值)。如图15:   图15   6、运行测试程序,观察LED的状态是否如测试程序所设定。如图16:   图16          下面以标准字符设备驱动框架来讲述LED驱动的编写方法。        标准字符设备驱动编写主要是实现注册模块(module_init)和注销模块(module_exit)两个函数。注册模块主要实现外设初始化以及调用int register_chrdev(unsigned int major, const char *name, struct file_operations *fops)函数来注册设备,为此需要给将要注册的设备分配主设备号major(如果major为0,则表示动态分配主设备号,分配的主设备号将作为register_chrdev()函数的返回值)、为设备驱动命名(此处的命名无需和mknod生成设备节点时命名一致,应用程序调用设备文件时是以mknod所命的名字为准的,但是在mknod时需要和注册设备时采用同样的设备号)以及实现file_operations结构体。在此我们只需实现ssize_t (*write) (struct file *, const char __user *buf, size_t count, loff_t *)函数即可,buf指向将要写入设备的数据,count指的是要写入设备的数据个数。在open设备时可以调用int (*open) (struct inode *, struct file *)函数可以打印主次设备号,在close时调用int (*release) (struct inode *, struct file *)函数释放设备。最后在注销设备(module_exit)时可以恢复外设配置并调用int unregister_chrdev(unsigned int major, const char *name)函数注销设备,此时的major和name应该和注册时一致。   下面讲述一下如何测试编写好的LED驱动:   1、按上一章中动态加载内核模块的操作方法,编译出tiny210_led.ko(驱动源代码为tiny210_led.c)内核模块。   2、对编写好的测试程序进行交叉编译(此处需记下open函数调用的设备名参数,以备下面mknod生成设备节点时分配设备名)。   3、将tiny210_led.ko内核模块和交叉编译生成的可执行程序(led)拷贝到/home/nfs路径中并在tiny210开发板上挂载nfs路径。   4、动态加载模式加载tiny210_led.ko模块,并观察设备号(编写驱动时我采用的是动态分配的方式分配设备号),如图17:   图17   5、mknod生成设备节点文件(注意设备名称和设备号),如图18:   图18   6、运行测试程序观察实验现象,如图19:   图19