Linux驱动学习笔记(二)
字符设备基础
1.Linux规定每一个字符设备或者块设备都必须有一个专属的设备号。一个设备号由主设备号和次设备号组成,主设备号用来表示某一类驱动,如鼠标,键盘都可以归类到USB驱动中。而次设备号是用来表示这个驱动下的各个设备,比如第几个鼠标,第几个键盘等。所以开发字符驱动程序,申请设备号是第一步,只有有了设备号,才可以向系统注册设备。Linux中使用一个名为dev_t的数据类型表示设备号,dev_t定义在include/linux/types.h里面。如下图所示,通过定义可以看出dev_t是u32类型,也就是unsigned int类型。所以设备号是一个32位的数据类型,其中高12位为主设备号,低20位为次设备号:
在文件include/linux/kdev_t.h中提供了几个操作设备号的宏定义,如下图所示:
宏MINORBITS表示次设备号位数,一共20位;宏MINORMASK用于计算次设备号;宏MAJOR表示从dev_t中获取主设备号,本质是将dev_t右移20位;宏MINOR表示从dev_t中获取次设备号,本质是取低20位的值;宏MKDEV用于将主设备号ma和次设备号mi组成成dev_t类型的设备号。在编写字符设备驱动代码的时候,可以静态分配设备号或者动态分配设备号。静态分配设备号指的是开发人员自己指定一个设备号,比如选择50这个设备号作为主设备号,因为有些设备号可能已经被系统使用了,在指定设备号的时候就不能再使用这些已经被系统使用的设备号了,使用命令“cat /proc/devices"命令可以查看当前系统中已经使用了哪些设备号(这个文件中存储了已经分配的主设备号,不会记录次设备号)。动态分配设备号指的是系统会自动给我们选择一个还没有被使用的设备号,这样就自动避免了设备号分配冲突的问题。在内核中,提供了动态分配设备号和静态分配设备号的函数,定义在include/linux/fs.h里面。静态分配函数:int register_chrdev_region(dev_t, unsigned, const char *);,其中参数1是设备号的起始值,类型是dev_t,比如MKDEV(100,0),表示起始主设备号100,起始次设备号为0;参数2是次设备号的数量,表示在主设备号相同的情况下有几个次设备号;参数3是设备的名称。该函数分配成功返回0,失败返回值小于0。动态分配函数int alloc_chrdev_region(dev_t*, unsigned, unsigned, const char *);,其中参数1用来保存自动申请到的设备号;参数2是次设备号的起始地址,次设备号一般从0开始,所以这个参数一般设置成0;参数3是要申请的设备号的数量,分配的次设备号是从起始此设备号开始递增的,例如起始次设备号为0,需要分配3个,那么将得到次设备号0、1、2;参数4是设备的名字。该函数分配成功返回0,失败返回值小于0。设备号释放函数:extern void unregister_chrdev_region(dev_t, unsigned);,注销字符设备以后要释放掉设备号,其中参数1是要释放的设备号,参数2是释放的设备号的数量,该函数释放的设备号是连续的。
2.Linux中,使用cdev结构体描述一个字符设备,cdev结构体定义在内核源码目录下的include/linux/cedv.h文件中,如下图:
其中struct file_operations在include/linux/fs.h中定义,是 Linux 内核中用于描述文件操作的核心数据结构,定义了内核如何操作文件(比如打开、读、写、关闭文件等)。如下图:
Linux有一个很重要的概念叫一切皆文件,也就是Linux中的设备就像普通的文件一样,访问一个设备就好像是在访问一个文件。在应用程序中可以使用open,read,write,close,ioctl这个几个系统调用来操作驱动。当我们在应用程序中调用open函数的时候,最终会去执行驱动中的open函数,所以file_operations将系统调用和驱动程序连接起来了。与cdev相关的函数如下(均在fs/char_dev.c中定义):
- cdev_init函数:该函数用于初始化cdev结构体成员,建立cdev和file_operations之间的联系。函数原型如下:
- cdev_add函数:该函数用于像系统添加一个cdev结构体,也就是添加一个字符设备,其中第一个参数是待添加的设备对应的cdev结构体,第二个参数为待添加设备的设备号,第三个参数是添加的数量,一般为1:
- cdev_del函数:该函数用于从系统中删除一个字符设备:
3.设备节点:本着Linux中一切皆文件的思想,每个设备在Linux系统中都有一个对应的“设备文件"代表他们,应用程序通过操作这个“设备文件",便可以操作对应的硬件。如下代码所示:fd=open("/dev/hello",O RDWR);,这个“设备文件"就是设备节点,所以Linux设备节点是应用程序和驱动程序沟通的一个桥梁,设备节点被创建在dev目录下,如下图:
上图中tty就是设备节点,位于/dev/目录下,c表示他是一个字符设备,5是主设备号,0是次设备号。Linux可以通过设备号来找到他对应的file_operations结构体,并通过主次设备号找到这个设备是同类设备中的第几个,这样就确定了是哪个驱动程序。Linux下创建节点的方式有两种:1.手动创建,可以通过命令mknod创建设备节点。mknod命令格式为:mknod 设备节点名称 设备类型(字符设备用c,块设备用b) 主设备号 次设备号,例如mknod /dev/test c 236 0(这个命令在命令行使用,可参考讯为Linux驱动视频第二期P8);2.在注册设备的时候自动创建,可以通过mdev机制实现设备节点的自动创建与删除。Linux中可以通过udev来实现设备节点的创建与删除,udev是一个用户程序,可以根据系统中设备的状态来创建或者删除设备节点,比如当驱动程序成功加载到Linux时会自动在/dev目录下创建对应的设备节点,当驱动程序卸载的时候会自动删除/dev目录下设备节点。在嵌入式Linux中我们使用的是mdev,mdev是udev的简化版本。在使用busybox构建根文件系统的时候,busybox会自动创建mdev。udev会去/sys/class/xxx/中查询,然后生成对应的/dev/yyy设备节点。想要自动创建和删除设备节点,需要涉及以下函数:class_create函数定义在include/linux/device/class.h文件当中,是一个宏定义,使用这个函数会在/sys/class下创建文件,如下图所示:
class_create一共有两个参数,第一个参数owner一般为THIS_MODULE,第二个参数name是类的名字,也即/sys/class/xxx中的xxx。不能多次创建同名的设备类,因为类名必须在系统中是唯一的,如果尝试创建重复的类,内核会返回错误。使用class_create创建好类以后,还需要使用device_create函数在类下面创建一个设备,定义在include/linux/device.h文件当中。如下所示:
其中第一参数class表示这个设备创建在哪个类下面,一般为使用class_create函数得到的返回值。第二个参数parent是父设备,一般设置成NULL,也就是没有父设备。第三个参数dev_t是设备号。第四个参数drvdata是设备可能会用到的数据,可设置成NULL。第五个参数fmt是设备节点的名字,例如将这个名称设为yyy,那么会生成设备节点/dev/yyy,并且会生成/sys/class/xxx/yyy文件。在卸载驱动时,要将通过class_create和device_create创建的class和设备删除。使用device_destroy函数可以删掉创建的设备,函数原型为:extern void device_destroy(struct class *cls, dev_t devt);,参数class是要删除的设备所处的类,dev_t是要删除的设备号。使用class_destroy函数可以删掉创建的类,函数原型为:extern void class_destroy(struct class *cls);,参数class是要删除的类(可参考讯为Linux驱动视频第二期P9)。
4.内核空间和用户空间的内存是不能互相访问的。但是很多业务程序都需要和内核交互数据,比如应用程序使用read函数从驱动中读取数据,使用write函数向驱动中写数据。这就要需要借助copy_from_user和copy_to_user这两个函数完成数据传输。分别是将用户空间的数据拷贝到内核空间以及将内核空间的数据拷贝到用户空间。这俩个函数定义在了include/linux/uaccess.h文件下。如下图:
copy_from_user函数用于把用户空间的数据复制到内核空间, to是指向内核空间的指针,from是指向用户空间的指针,n是待复制数据的字节数,该函数复制成功返回0。copy_to_user函数用于把内核空间的数据复制到用户空间, to是指向用户空间的指针,from是指向内核空间的指针,n是待复制数据的字节数,该函数复制成功返回0。
5.文件私有数据的概念在Linux驱动中有着非常广泛的使用,即:可以将设备需要用到的数据打包到自定义的结构体中而不是全部定义成散乱的全局变量,并在用file_operations结构体中的成员函数open打开设备时,用open函数的参数成员file结构体的成员file->private_data指向当前设备的私有数据,这样设计方便代码移植(具体可参考讯为代码07_private_data及讯为Linux驱动视频第二期P14),file结构体如下图所示:
6.container_of 宏的功能是根据给定的结构体成员的地址,返回该成员所在结构体的首地址。这个宏通常用于内核编程中,特别是需要从结构体的某个成员指针反向计算出整个结构体指针的场景。该宏在include/linux/kernel.h中定义,原型为:container_of(ptr, type, member),其中第一个参数ptr是结构体变量中某个成员的地址,第二个参数type是结构体的类型,第三个参数member是该结构体变量的具体名字。
7.在Linux中,把无法归类的五花八门的设备定义成杂项设备。相对与字符设备来说,杂项设备主设备固定为10,而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,比较浪费主设备号。杂项设备会自己调用class_create()和device_create()来自动创建设备节点,所以可以把杂项设备看成是字符设备的一种,但比平常写的字符设备降低了难度并节约了主设备号。杂项设备使用结构体miscdevice描述,定义在include/linux/miscdevice.h中,如下图:
其中次设备号minor一般使用宏MISC_DYNAMIC_MINOR,表示自动分配次设备号,杂项设备主要依赖次设备号来管理不同的杂项设备,name用来指定杂项设备的名称。注册杂项设备使用函数misc_register函数,卸载杂项设备使用misc_deregister函数,这两个所数均定义在include\linux\miscdevice.h文件当中。int misc_register(struct miscdevice *misc),其参数为杂项设备的结构体指针,注册成功返回0,失败返回负数;int misc_deregister(struct miscdevice *misc),其参数为杂项设备的结构体指针,卸载成功返回0,失败返回负数。
8.Linux驱动错误处理:在驱动程序中,一般使用goto语句来进行错误处理,并遵循“先进后出”原则,即顺序执行时排在前面的语句对应的错误处理的goto语句应排在更后面。内核中保留了地址0xffff_ffff_ffff_f000~0xffff_ffff_ffff_ffff(64位系统)用来记录错误码,这段地址和Linux的错误码是一一对应的,内核基本错误码对应的宏定义在include/uapi/asm-generic/errno-base.h文件中。内核中的函数常常返回指针,如果内核返回一个指针,那么就有三种情况:合法指针、NULL指针和非法指针。使用IS_ERR函数去检查函数的指针类型的返回值,如果地址落在0xffff_ffff_ffff_f000~0xffff_ffff_ffff_ffff(64位系统),表示该函数执行失败,IS_ERR返回1,同时该函数返回的错误地址对应一个Linux的错误码。如果想知道这个地址是哪个错误码,就用PTR_ERR函数来转化。其中IS_ERR和PTR_ERR函数定义在include/linux/err.h当中。例如:if(IS_ERR(p_return)){ret=PTR_ERR(p_return);}(可参考讯为Linux驱动视频第二期P18)。
9.一个LED驱动示例:首先找到led灯是哪个 GPIO控制,例如通过RK3568底板原理图可知LED9对应的GPIO为 GPIO0_B7;然后配置GPIO的复用关系为GPIO端口(复用关系寄存器的基地址为0xFDC20000,要操作的地址为0xFDC20000+0x000C);配置GPIO方向为输出(GPIO0的基地址为0xFDD60000,对应方向寄存器的地址为0xFDD60000+0x0008);配置GPIO的数据寄存器(数据寄存器的地址为0xFDD60000+0x0000,例如可向该数据寄存器写入0x8000c040表示亮灯,写入80004040表示灭灯)。需要注意的是,上面提到的地址都是物理地址,但在驱动程序中需要将物理地址转换为虚拟地址并使用,涉及的函数有:static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size),其中offset为要映射的起始物理地址、size为要映射的空间的大小;void iounmap(void * addr); 函数用于取消ioremap()所做的映射,参数addr即为ioremap返回的虚拟地址(可参考讯为Linux驱动视频第二期P19)。