第12课 字符设备驱动

来自百问网嵌入式Linux wiki
跳转至: 导航搜索

1,字符设备驱动程序概念介绍

UBOOT: 启动内核 内核: 启动应用程序。 应用程序:会涉及很多文件的读写操作。如点LED,按键等。 对于写应用程序的人不会要求查芯片手册。而是用open,read,write等标准接口来调用驱动程序。


如: 应用程序:open read write

       驱动程序:led_open	  led_read	   led_write

(最终依赖驱动程序框架来从应用程序对应到驱动程序)

最终应用程序中的open如何调用到驱动程序中的led_open,这些open等函数是C库实现的。当应用 程序调用这些 open 、write系统调用接口时,它会进入到内核,open 、read执行时,实质上是执行一条 swi 指令,swi后面加上某个值,这个汇编指令就会引发一个异常,当发生这个异常时,就进到内核的异常处理函数中。内核调用接口根据发生中断的原因,调用不同的处理函数。

				int main()
				{
					int fd1,fd2;
					int val = 1;
				
					fd1 = open("/dev/led",O_RDWR);
					write(fd1,&val,4);
					
					fd2 = open("hello.txt",O_RDWR);
					write(fd2,&val,4);
				}

如这个程序,用open时,传进来的是 val1 (swi val1),用read时,传来的是val2等。这时内核的系统调用接口(system call interface)根据传进来的不同值val1或val2,去调用 sys_open,sys_read,sys_write (这一层叫 VFS:虚拟文件系统)。 根据打开的具体文件,有具体属性去寻找不同的驱动程序。调用这些不同驱动程序里的open,read函数。

Ldd chapter12 001.png

Sysfs:

    LINUX2.6内核开发了全新的设备模型。它采用 sysfs 文件系统,其类似于 proc 文件系统,用于将系统中设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。


内核对象机制: Kobject:

    LINUX2.6引入的新的设备管理机制。通过这个数据结构使所有设备在底层都具有统一的接口。Kobject 提供基本的对象管理,是构成 LINUX2.6 设备模型的核心结构,其与 sysfs 文件系统紧密关联,每个在内核中注册的 kobject 对象都对应于 sysfs 文件系统中的一个目录。Kobject 通常通过 kset 组织成层次化的结构,kset 是具有相同类型的 kobject 的集合。

设备模型结构(/include/linux/device.h):

1,devices-设备结构:

2,drivers-驱动结构:

3,buses-总线结构:

4,classes-设备类结构:

Ldd chapter12 002.png 给驱动模块提供参数: Ldd chapter12 003.png 在内核中加入新驱动:将驱动编译进内核。

 若将 pxa_smbus.c 添加到内核:
 1,先将其复制到 /drivers/char 目录,更改该目录下的 Kconfig,增加:

Ldd chapter12 004.png Ldd chapter12 005.png 2,在该目录下的 Makefile 中增添下行: Ldd chapter12 006.png 3,进入源代码目录,执行 make menuconfig: Ldd chapter12 007.png 驱动程序原理图: Ldd chapter12 008.png


2,LED驱动程序

应用程序: open read write

驱动程序:led_open led_read led_write

(最终依赖驱动程序框架来从应用程序对应到驱动程序)


“应用进程” 和 “驱动程序”如何联系:

内核定义了一个 struct file_operations 结构体,这个结构的每一个成员的名字都对应着一个系统调用。 用户进程利用系统调用在对设备文件进行诸如读写操作时,系统调用通过设备文件的主设备号找到相应的设备驱动 程序,然后读取这个数据结构相应的函数指针,接着将控制权交给该函数。 Ldd chapter12 2LED 001.png Ldd chapter12 2LED 002.png

在上面的成员函数指针的形参中,struct file 表示一个打开的文件。Struct inode 表示一个磁盘上的具体文件。

Struct file数据结构如下: Ldd chapter12 2LED 003.png Ldd chapter12 2LED 004.png

一,内核和驱动过程:

  • 1,最终是要写出驱动程序中的:led_open led_read led_write等函数

Ldd chapter12 2LED 005.png 其中的结构:struct inode 和 struct file

Struct inode:


  • 2,接着要将这些函数告诉内核。

从上到下找到这个LED的(如上图),用它前,要让内核知道有LED这个东西.

①.定义一个结构体告诉内核。file_operations 结构,填充这个结构。 Ldd chapter12 2LED 006.png 用户程序中有什么接口,这个 file_operations 结构中有相应功能的成员。 Ldd chapter12 2LED 007.png

②.有了这个结构后,将这个定义的结构告诉内核。用一个函数register_chrdev()告诉内核,即 “注册”。

int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)

术语为“注册驱动程序”。就是告诉内核。

参1:主设备号。

参2:名字,可以随便写。

参3:定义的结构(将这个结构告诉内核) Ldd chapter12 2LED 008.png

③.上面的注册驱动的函数由谁调用:"驱动入口函数"来调用 register_chardev(),有不同的入口函数。 Ldd chapter12 2LED 009.png 上面的“first_drv_init”即为“驱动入口函数”。

有第1个入口函数,第2个入口函数,不同的驱动程序有不同的“驱动入口函数”。这些驱动入口函数有不同名字.

内核通过修饰不同的入口函数让内核知道注册的驱动函数是由哪个“入口函数”调用。

④."入口函数"得修饰。一个 宏“module_init”来定义。 800px module_init 就是定义一个结构体,这个结构体中有一个函数指针,指向“入口函数”。 当安装一个驱动程序时,内核会自动找到这样一个结构体,调用里面的“函数指针”,就指向了 “入口函数”。“入口函数”就将结构“file_operations first_drv_fops”告诉内核。


二,注册驱动函数细节:

①,内核如何找到相应的“file_operations”结构:设备类型 + 主设备号 800px (上面主设备号为 7, 次设备号为 4 。)

应用程序--->

打开一个设备文件 open("dev/xxx"),或read,write.打开xxx有属性:c字符型设备。

major(主设备号)

Mior (次设备号)

800px register_chrdev注册驱动程序时,参数有:

参1,“major”:主设备号

参2,“名字”:驱动的名字(随便写)。

“file_operations first_drv_fops结构体(read、write、open等成员)”


应用程序如何最终找到register_chrdev注册的东西(file_operations first_drv_fops结构),具体:

应用程序 open("dev/xxx") 如何打开结构体first_drv_fops中的open成员?

用“设备类型->字符类型(这里为字符类型)”+“主设备号”

VFS系统就是根据打开的文件"open("dev/xxx")中的xxx,"里面的属性(设备类型+主设备号),根据这两个属性,就能找到注册(register_chrdev)进去的“file_operations ”结构 (first_drv_fops)。

②.register_chrdev最简单的实现方法是:(register_chrdev的作用) Ldd chapter12 2LED 013.png

在一个内核数组(chardev)中,以“主设备号”major为索引,找到某一项,在这一项中把 “file_operations”结构(这里是“first_drv_fops”)填充进去(挂进去)。


总结过程:驱动程序和应用程序的联系

  • 从应用层看:

首先,APP用 Open(“dev/xx”,O_RDWR)打开设备文件后,会得到此设备文件的属性,知道属性中的“设备类型”和“主设备号”。然后 VFS 层通过“设备类型”(如字符设备)去找内核中的“chardev”这个数组。再通过从APP中得到的“主设备号”以此为索引从内核的“chardev”数组中知道相应的“file_operations”结构。这个结构 是驱动程序“register_chardev”注册到内核的,这样从索引又找到了它。这个结构中有相应的“读”“写” 成员中,这些成员就对应着硬件的读和硬件的写操作等待如(led_read,led_write)。(VFS层工作过程?)

  • 从驱动程序看:如操作LED:

首先实现LED的读写(led_read,led_write).然后定义一个“file_operation”结构(操作这个LED用),比如此结构中的Open成员就指向“led_open”,结构中的write就指向对硬件LED的操作“led_write”等。再然后,在写的驱动入口函数中,用“register_chardev”把上面定义的“file_operation”结构体放到内核的“chardev”字符设备数组中去,是放到“chardev”字符设备数组下标为“major”主 设备号编号处。


三.注册是用 register_chardev把对相应硬件的操作定义的“file_operations”结构通过注册时定义的形参“主设 备号”放到内核的字符

设备数组中以“主设备号”为索引下标的数组空间去了。 Ldd chapter12 2LED 014.png 那么要卸载这个驱动程序时,就要把这个硬件对应的“file_operations”结构从内核字符设备chardev 数组中取出来(不挂接此硬件 file_operations结构的地址)。这个过程用“unregister_chrdev”实现。 Ldd chapter12 2LED 015.png 只有两个参数,major和名字。删除驱动是将结构 file_operations从内核“字符设备数组”中拖出来。如何让内核知道这个“first_drv_exit”普通函数,也是修饰一下:module_exit();所谓修饰就是用一个宏来定义一个结构体,结构体中有某个成员“函数指针”指向它。当卸载驱动程序时,内核就会自动去调用这个结构体中的这个“函数指针”成员。


四,写一个简单的字符驱动程序: Ldd chapter12 2LED 016.png

  • 1,一个简单的框架:

Ldd chapter12 2LED 017.png Ldd chapter12 2LED 018.png

  • Makefile:

Ldd chapter12 2LED 019.png Ldd chapter12 2LED 020.png Ldd chapter12 2LED 021.png Ldd chapter12 2LED 022.png Ldd chapter12 2LED 023.png 因为一个函数前没有加“static”所以一直出现下面的错误: Ldd chapter12 2LED 024.png Ldd chapter12 2LED 025.png 刚才上面的没有加“static”。 Ldd chapter12 2LED 026.png

驱动程序中设备号: 自动分配主设备号 手工分配主设备号

应用程序设备节点:手动通过主设备号创建设备节点 mknod /dev/??? 设备类型 主设备号 次设备号 自动创建设备节点。应用程序中有一个"udev"机制,对于busybox来说是mdev。


册一个驱动程序后,会在 /sys/ 目录下生成相关设备的硬件信息。mdev程序会根据这些sys目录下提供的信息 自动创建设备节点。所以要在驱动程序中提供这些sys目录下的信息。

系统信息要先创建类,再然后在类下面创建设备。创建设备信息时,会提供主设备号和名字: Ldd chapter12 2LED 027.png 内核中有设备加载或卸载掉时,就会通过“/proc/sys/kernel/hotplug”所指示的应用程序(上面 是/sbin/mdev)去自动加载或卸载设备节点。

Ldd chapter12 2LED 028.png 第一个参数指定类的所有者是哪个模块,第二个参数指定类名。

	struct class_device *class_device_create(
	                    struct class        *cls,      设备类
	                    struct class_device *parent,
	                    dev_t               devt,
	                    struct device       *device,   类下的设备
	                    const char          *fmt, ...) 类下设备的名字

Ldd chapter12 2LED 029.png 第一个参数指定所要创建的设备所从属的类,第二个参数是这个设备的父设备,如果没有就指定为NULL,第三个参 数是设备号,第四个参数是设备名称,第五个参数是从设备号。 将*device对应的逻辑设备(class_device)添加到*cls所代表的设备类中。在*cls所在目录下,建立代表逻辑设备的 目录。


完善点亮LED:

要点亮LED灯:

1.写好框架。

2.完善硬件操作:


一,分析:完善硬件操作

①.看原理图,确定引脚。

②.看2440的芯片手册。查如何操作引脚。

③.写代码。

写单片机时是直接操作那个物理地址。

驱动程序中不能直接操作物理地址了,是操作虚拟地址。用 ioremap 函数来将物理地址映射成虚拟地址。

 先查原理图

1,先看主板的原理图:MOTHERBOARD V3.pdf Ldd chapter12 2LED 030.png Ldd chapter12 2LED 031.png 再看核心板的原理图: Ldd chapter12 2LED 032.png Ldd chapter12 2LED 033.png

从原理图上看,GPF0~3引脚输出 0 ,LED会亮。

1.查原理图看到三个LED灯接的引脚是 GPF4~6,则再看2440芯片手册操作相应的寄存器。

在芯片手册中搜索 GPFCON 寄存器 800px 2.先将 GPF0~3配置成 Output 输出引脚。 Ldd chapter12 2LED 035.png 3.操作GPFCON这个寄存器的物理地址是:0x5600 0050 再操作GPFDAT这个寄存器。

 即可。


二,写代码:硬件上的操作:

配置引脚:操作 GPFCON 寄存器。(放到 open 函数中做)

设置:设备 GPFDAT 寄存器,让引脚输出高、低电平。(放到 wirte 函数中做)


 1,映射物理地址: Ldd chapter12 2LED 036.png 先定义两个变量,和寄存器相对应。他们分别的物理地址要分别映射成虚拟地址。 在入口函数中映射(避免打开一次要映射一次)。 用 ioremap (开始地址,结束大小) Ldd chapter12 2LED 037.png 先映射GPFCON为虚拟地址。F 寄存为 GPFCON:0x56000050, GPFDAT:0x56000054, GPFUP:0x56000058, Reserved:0x5600005c 共4个,2440为32位位CPU,所以4个4字节为 16字节,故这里映射16字节。 Gpfdat = gpfcon + 1; 这里指针的加1是以上面“unsinged long”为单位的。

0x56000050这是: Ldd chapter12 2LED 038.png GPFCON寄存器的地址,后面 16是16字节,这里刚好有4个寄存器,每个4字节,所以就定成16字节了。 这里加1不是加了4字节,指针的操作是以 指针 所指向的长度为单位的,这里的指针是:“ Ldd chapter12 2LED 039.png 在入口函数中用 ioreamp() ,则在出口函数中要用 iounmap(),将建立的映射去掉。 Ldd chapter12 2LED 040.png

以上就是物理地址与虚拟地址的映射,以后要操作寄存器时,就用 gpfcon 和 gpfdat 两个指针来操作。 这两个指针指向的是虚拟地址。 800px


 2,设置引脚为输出引脚:先清零,再或上“01”。

之后以为配置成“输出引脚”,是原理图上LED一端接了高电平,只有2440这端输出低电平,才有电流通过。

设置成输出引脚: Ldd chapter12 2LED 042.png GPF0~3 先清零,再或上 1 。就是输出引脚了。在“first_drv_open”中初始化GPFCON引脚电平为输出。 Ldd chapter12 2LED 043.png

gpfcon本身就代表了GPFCON寄存的地址(虚拟地址):

gpfcon = gpfcon & (~((0x3<<0*2) | (0x3<<1*2) | (0x3<<2*2) | (0x3<<3*2)));

“0x3”二进制为“11” GPF0占 bit0~1,则要将它清0则为:

~(0x3<<0)

GPF1 占 bit2~3,则要清0:

~(0x3<<2

GPF2 占 bit4~5,清0则为:

~(0x3<<4)

3,将用户空间的数据传到内核空间:

val如何传进来,就是 *buf ,应用程序中调用的时候: Ldd chapter12 2LED 044.png Ldd chapter12 2LED 045.png 这个值便是 应用程序 传进来的buf和count.用一个函数来取这些应用程序传过来的值。

从用户空间到内核空间的数据传递函数 copy_from_user()。

从内核空间向用户空间传递数据用函数 copy_to_user(); Ldd chapter12 2LED 046.png 将传来的buf拷贝到val(定义的)空间,拷贝的长度是count(应用程序传进来的)。

接着根据这个值判断:如果这个值 val==1,打开LED(输出低电平)。否则关掉LED灯(输出高电平)。打开和关闭LED,就是操作寄存器。 Ldd chapter12 2LED 047.png Ldd chapter12 2LED 048.png 当传进来的值 val 为1,依原理图就要设置 GPF0~3 这些引脚输出0.那就是清位。要关LED时,就使引脚输出高电平,就或上某一位。这里 val = 1,是通过copy_from_user()将"first_drv_write()"中的用户空间的buf值传进的。另一种方法是用ioctl()。驱动测试程序 firstdrv_test 打开 /dev/led_test 设备节点时,就会通过设备节点的主设备号进入到内核的file_operations结构数组中找到与这个驱动的主设备号相关的结构体,进而关联到sys_open等函数上去。 Ldd chapter12 2LED 049.png 上面是测试程序,打开'on'时,val=1,从write(fd,&val,4); 中将这个val=1写到fd关联的设备节点“/dev/led_tset”,进而通过它的主设备号进入到内核,关联到: Ldd chapter12 2LED 050.png Ldd chapter12 2LED 051.png First_drv_fops 结构中去,write关联到“int first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)”,这样,val=1这个值就传给了“const char __user *buf”。可以看到这是一个用户空间的数据,接着通过“copy_from_user”将这个用户空间的值拷贝到内核空间。

以上程序就已经能同时打开或关闭4个LED了。要分别点亮4个LED时,可以通过程序测试程序中的 write()不同的val值对应不同的file_operations中的写操作。还有一个方法是利用“次设备号”。


总结:写一个驱动程序。

1.首先写驱动框架。

2.结硬件的操作。

看原理图--看芯片手册--写代码和单片机差不多,区别在于:

  • 单片机中直接使用物理地址。
  • 驱动程序中得用物理地址映射后的函数地址。

要单独控制某一个LED灯。 Ldd chapter12 2LED 052.png 1种方法是解析传进来的值,如传进来1点亮LED1,传进来2 val == 2,则点亮LED2.根据传进来的值执行不同动作。

2种方法:设备节点 Ldd chapter12 2LED 053.png 主设备号是帮我们在内核的 chardrv 数组中找到那一项,再根据这一项找到 file_operatios 结构: Ldd chapter12 2LED 054.png 但是上面的 次设备号 是留下来给我们用的,由我们管理。 Ldd chapter12 2LED 055.png 用 MINOR 取出次设备号。据不同次设备号来操作。次设备的操作完全由自已定义。 看“myleds.c”这个驱动程序。 Ldd chapter12 2LED 056.png

下面是内核提供的使用虚拟地址操作方式:封装好的。 Ldd chapter12 2LED 057.png Ldd chapter12 2LED 058.png Ldd chapter12 2LED 059.png


3,查询方式获取按键值驱动

Ldd chapter12 3KEY 001.png Ldd chapter12 3KEY 002.png Ldd chapter12 3KEY 003.png Ldd chapter12 3KEY 004.png

一,驱动框架实现:

Ldd chapter12 3KEY 005.png Ldd chapter12 3KEY 006.png Ldd chapter12 3KEY 007.png Ldd chapter12 3KEY 008.png

二,硬件操作:

1,看原理图:查找引脚定义。

查主板: Ldd chapter12 3KEY 009.png Ldd chapter12 3KEY 010.png Ldd chapter12 3KEY 011.png Ldd chapter12 3KEY 012.png 所以可以知道:KEY1 - GPF4

KEY2 - GPF5

KEY3 - GPF6

KEY4 - GPF7

2,设置 4 个引脚为输入引脚:从上面的原理图可以看到,低电平要从开关接地端输入。 要有电流流过,就是有压差,从上面的原理图上知道,KEY一端接了高电平,当KEY没按下时,这个KEY接2440的引脚都是高电平。当KEY按键按下,就接通了地,这时这些引脚就返回 0 。 Ldd chapter12 3KEY 013.png Ldd chapter12 3KEY 014.png Ldd chapter12 3KEY 015.png Ldd chapter12 3KEY 016.png 3,在read操作中,返回4个引脚的状态。然后将状态值从内核空间拷贝到用户空间。当驱动测试程序读设备节点时,调用到驱动中的“second_drv_read()”函数。 Ldd chapter12 3KEY 017.png 4,测试程序: Ldd chapter12 3KEY 018.png Ldd chapter12 3KEY 019.png Ldd chapter12 3KEY 020.png 判断copy_from_user、copy_to_user两个函数的返回值,正常返回0,出错则返回没有复制成功的字节数。出错时返回-EFAULT。


加载驱动: Ldd chapter12 3KEY 021.png KEY2 Ldd chapter12 3KEY 022.png KEY3 Ldd chapter12 3KEY 023.png KEY4 Ldd chapter12 3KEY 024.png 抖动的很厉害。

查询的按键方式因为里面有一个死循环while,会占用CPU资源: Ldd chapter12 3KEY 025.png 所以查询的方式不可取。查询是连续去读取按键值,去比较值是否有变化。这种方法很耗资源。


4,异常处理结构、中断处理结构