490 likes | 805 Views
Linux 系统驱动概述. 博创科技. Linux 系统驱动概述. 操作系统功能的划分 linux下的设备驱动的特点 Linux 设备的分类 典型的 Linux 字符型设备驱动结构 Linux 驱动中的数据结构 Linux 驱动中常用的操作函数 Linux 下的设备管理的问题. 操作系统功能的划分. 进程管理 内存管理 文件系统 设备控制 网络功能. linux 下的设备驱动的特点. linux 下的设备控制由驱动程序完成: Linux 下对外设的访问只能通过驱动程序 Linux 对于驱动程序有统一的接口,以文件的形式定义系统的驱动程序:
E N D
Linux系统驱动概述 博创科技
Linux系统驱动概述 • 操作系统功能的划分 • linux下的设备驱动的特点 • Linux设备的分类 • 典型的Linux字符型设备驱动结构 • Linux驱动中的数据结构 • Linux驱动中常用的操作函数 • Linux下的设备管理的问题
操作系统功能的划分 • 进程管理 • 内存管理 • 文件系统 • 设备控制 • 网络功能
linux下的设备驱动的特点 • linux下的设备控制由驱动程序完成: • Linux下对外设的访问只能通过驱动程序 • Linux对于驱动程序有统一的接口,以文件的形式定义系统的驱动程序: Open、Release、read、write、ioctl… • 驱动程序属于内核的一部分,可以使用中断、DMA等操作 • 驱动程序需要在用户态和内核态之间传递数据,是应用程序与内核及外设通讯的桥梁 • uClinux下可以在应用层直接访问外设,操作寄存器,但是无法处理中断——不推荐使用
Linux设备的分类 按照上述系统内核的功能,Linux中把系统的设备定义成如下三类: • 字符设备 • 支持面向字符的I/O操作 • 使用独立的缓冲区结构 • 顺序存取数据 • 块设备 • 仅支持面向块的I/O操作 • I/O操作都通过在内核地址空间中的I/O缓冲区进行 • 随机存取:支持几乎任意长度和任意位置上的I/O请求 • 网络设备
典型的Linux字符型设备驱动所包含的内容 • 初始化函数:xxx_init,向操作系统注册及硬件初始化(包括中断的request_irq和使能) • 主体代码 :file_operations里面注册的操作函数 • 中断处理函数 • 示例
Linux驱动中的数据结构- file_operations file_operations数据结构,定义在include/linux/fs.h中,驱动程序很大一部分工作就是要“填写”结构体中定义的函数。 lseek:移动文件指针的位置,只能用于随机存取设备 read:读操作 write:写操作,与read类似 readdir:取得下一目录节点,文件系统相关的设备驱动程序使用 select:选择操作,如果没有提供将会认为设备已经准备好 ioctl:读、写以外的其它操作,参数为自定义的的命令数 mmap:内核空间到用户空间的映射 open:打开设备,为I/O操作做准备 release:即close操作
Linux驱动中的数据结构- file_operations • Open函数 Open方法提供给驱动程序初始化设备的能力,从而为以后的设备操作做好准备,此外open操作一般还会递增使用计数,用以防止文件关闭前模块被卸载出内核。在大多数驱动程序中Open方法应完成如下工作: 递增使用计数 1.检查特定设备错误。 2.如果设备是首次打开,则对其进行初始化。 3.识别次设备号,如有必要修改f_op指针。 4.分配并填写filp->private_data中的数据。
Linux驱动中的数据结构- file_operations • Release函数 与open方法相反,release 方法应完成如下功能: 1.释放由open分配的filp->private_data中的所有内容 2.在最后一次关闭操作时关闭设备 3.使用计数减一
Linux驱动中的数据结构- file_operations • Read和Write函数 • read 方法完成将数据从内核拷贝到应用程序空间,write方法相反,将数据从应用程序空间拷贝到内核。对于者两个方法,参数filp是文件指针,count是请求传输数据的长度,buffer是用户空间的数据缓冲区,ppos是文件中进行操作的偏移量,类型为64位数。由于用户空间和内核空间的内存映射方式完全不同,所以不能使用象memcpy之类的函数,必须使用如下函数: • unsigned long copy_to_user (void *to,const void *from,unsigned long count); • unsigned long copy_from_user(void *to,const void *from,unsigned long count);
Linux驱动中的数据结构- file_operations • ioctl函数 • ioctl方法主要用于对设备进行读写之外的其他控制,比如配置设备、进入或退出某种操作模式,这些操作一般都无法通过read/write文件操作来完成,比如在UP-NETARM2410-S中的SPI设备通道的选择操作,无法通过write操作控制,这就是ioctl操作的功能。 • 驱动程序中定义的ioctl 方法原型为: int (*ioctl) (struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg) inode 和 filp两个指针对应应用程序传递的文件描述符fd,cmd不会被修改地传递给驱动程序,可选的参数arg则无论用户应用程序使用的是指针还是其他类型值,都以unsigned long的形式传递给驱动。
Linux驱动中的数据结构 • inode结构: • 提供关于设备文件的/dev/xxx的信息 • file结构: • 提供关于被打开的文件的信息
Linux驱动中常用的操作函数 • 申请中断: • int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long irq_flags, const char * devname, void *dev_id) • 释放中断: • void free_irq(unsigned int irq, void *dev_id); • 申请内存:void * kmalloc(unsigned int len, int priority); • 释放内存:void kfree(void * obj); • 访问I/O端口:inb,outb,inw,outw;inl,outl • 时钟、定时器有关的系统函数:add_timer,del_timer,init_timer • 打开和关闭中断允许:cli(),sti() • 内核打印:printk
设备管理的问题 如今,Linux 支持很多不同种类的硬件。这意味着/dev中都有数百个特殊文件来表示所有这些设备。而且,这些特殊文件中大多数甚至不会映射到系统中存在的设备上
Linux设备驱动的路径 • Linux的设备以文件的形式存在于/dev目录下 • 设备文件是特殊文件,使用ls /dev -l命令可以看到: • c-字符设备 • b-块设备 • l-符号连接 crw------- 1 root root 10, 7 Aug 31 2002 amigamouse1 crw------- 1 root root 10, 134 Aug 31 2002 apm_bios brw-rw---- 1 root disk 29, 0 Aug 31 2002 aztcd lr-xr-xr-x 1 root root 9 Dec 26 05:52 tty0 -> /dev/vc/0
主设备号和次设备号 • 主设备号标识设备对应的驱动程序 • 一个驱动程序可以控制若干个设备,次设备号提供了一种区分它们的方法 • 系统增加一个驱动程序就要赋予它一个主设备号。这一赋值过程在驱动程序的初始化过程中进行: 对于字符设备: int register_chrdev(unsigned int major, const char*name,struct file_operations *fops); • 对于查看/dev目录下的设备的主次设备号可以使用如下命令: [/mnt/yaffs]ls /dev -l crw------- 1 root root 5, 1 Jan 1 00:00 console crw------- 1 root root 5, 64 Jan 1 00:00 cua0
动态分配设备号 • 在Documentation/device.txt文件中可以找到已经静态分配给大部分设备的列表 • 由于许多数字已经分配了,为新设备选择一个唯一的号码是很困难的 • 如果调用register_chrdev时的major为零,函数就会选择一个空闲号码并做为返回值返回
动态分配的问题 动态分配的主设备号不能保证总是一样的,无法事先创建设备节点 • 可以从/proc/devices读取 cat /proc/devices • 利用脚本动态创建设备文件节点
使用设备文件系统--devfs • 在Linux 2.4的内核里引入了devfs来解决linux下设备文件管理的问题 • 在驱动程序中通过devfs_register()函数创建设备文件系统的节点 其原型为: devfs_register(devfs_handle_t dir, const char *name, unsigned int flags,unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info) • 系统启动的时候mount设备文件系统 • 所有需要的设备节点都由内核自动管理。/dev目录下只有挂载的设备 • 注:在linux-2.6.12之后的内核的解决办法:udev文件系统 • http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html
使用设备文件系统-- udev • udev 完全在用户态 (userspace) 工作,利用设备加入或移除时内核所发送的hotplug 事件 (event) 来工作。 • 关于设备的详细信息是由内核输出 (export) 到位于 /sys 的 sysfs 文件系统的。所有的设备命名策略、权限控制和事件处理都是在用户态下完成的。 • 与此相反,devfs 是作为内核的一部分工作的。
创建设备节点--mknod • mknod /dev/xxx c major minor • 关于主次设备号: major + minor • 上层应用对驱动的调用: • fd=open(“/dev/xxx”,...); file_operations xxx_open • read(fd,buffer,count); file_operations xxx_ read
Linux驱动程序原理 • fd=open(“/dev/xxx”,...) file_operations xxx_open • write(fd,buffer,count) file_operations xxx_ write • read(fd,buffer,count) file_operations xxx_ read
Linux下的驱动加载方式 • 一种是直接编译到内核,当内核启动之后,新的驱动程序随之运行; • 二是编译为模块,动态加载运行 • 对模块操作需要使用module-utiles: • insmod将编译的模块直接插入内核 • rmmod从内核中卸载模块 • lsmod显示已安装的模块 • gcc编译参数: -D__KERNEL__ -DMODULE –I$(KERNELDIR_INCLUDE) • 在调试的过程中一般使用模块动态加载的方式,它的调试效率较高。当驱动调试完成后,在发行的过程就集成进内核。但编译进内核是某些驱动运行的唯一方法。例如:console驱动,flash驱动和对至少一种文件系统的支持等等。
Linux下的驱动加载方式 • module_init:insmod时自动调用,负责模块初始化工作。 • 对应的操作是module_init • module_exit:卸载时调用,负责清除工作。 • 对应的操作是module_exit • 参见:kernel/include/linux/init.h
module_init(1) include/linux/init.h中 #define module_init(x) __initcall(x); #define __initcall(fn) \ static initcall_t __initcall_##fn __init_call = fn static initcall_t __initcall_name_of_initialization_routine __init_call = name_of_initialization_routine
module_init(2) • include/linux/init.h中定义: #define __init_call __attribute__ ((unused,__section__ (".initcall.init"))) typedef int (*initcall_t)(void); • arch/arm/vmlinux-armv.lds.in文件中: __initcall_start = .; *(.initcall.init) __initcall_end = .; . = ALIGN(4096); __init_end = .; • init/main.c文件中定义了do_initcalls函数
__init宏 在include/linux/init.h中 对于非模块加载的驱动程序: #define __init __attribute__ \ ((__section__ (".text.init"))) • 通过__init,会把函数中的代码放到.text.init段。这个段在系统启动以后会被释放。在系统内核启动以后,会看到: Freeing init memory: 68K
Linux下驱动的调试 1、 使用printk函数 printk函数中可以使用附加不同的日志级别或消息优先级 2 、使用/proc文件系统 内核利用它向外输出信息 3、使用ioctl方法 ioctl系统调用会调用驱动的ioctl方法,我们可以通过设置不同的命名号来编写一些测试函数,使用ioctl系统调用在用户级调用这些函数进行调试。 4、使用strace命令进行调试 strace命令是一个功能强大的工具,它可以显示用户空间的程序发出的全部系统调用,不仅可以显示调用,还可以显示调用的参数和用符号方式表示的返回值。
Linux下的中断驱动 • 中断概念 • 基本定义:中断,异常,中断源,中断嵌套,中断优先级等 • 中断向量:标识中断的无符号数,组成中断向量表。ARM有7个: • 复位:当复位电平有效时产生。程序跳转到复位异常处理程序处 • 未定义指令:遇到不能处理的指令时产生 • 软件中断:执行SWI指令产生。用于实现系统调用功能 • 指令预取中止:预取指令的地址不存在,或该地址不允许当前指令访问,发出中止信号,但指令被执行时产生异常 • 数据中止:数据访问指令的地址不存在,或该地址不允许当前指令访问时,产生数据中止异常 • IRQ:标准中断请求 • FIQ:快速中断请求
ARM的中断过程 • 中断的进入 • 将下一条指令的地址存入相应连接寄存器LR • 将CPSR复制到相应的SPSR中 • 根据中断/异常类型,强制设置CPSR的运行模式位 • 强制PC从相关的异常向量地址取下一条指令执行,从而跳转到相应的异常处理程序 • 从中断返回 • 将连接寄存器LR的值减去相应的偏移量后送到PC中 • 将SPSR复制回CPSR中 • 如果进入时设置了中断禁止位,那么清除该标志 • 中断返回 • 注意:给IRQ脚中断信号前,必须先打开该中断的使能寄存器和正确设置对应的屏蔽寄存器。当这两个寄存器都设置正确了,中断产生了,CPU保存当前程序运行环境,跳到中断入口,ARM芯片一般是0x18地址处。设置好中断向量,中断向量一般是个跳转语句,跳到正式的中断处理过程,在这里可以关闭所有中断,清中断,处理等等,然后退出。某些处理器一定要清中断,否则下次再给中断信号时就没有反应了。
中断相关函数说明 • int set_external_irq(int irq, int edge, int pullup); • 功能:设置中断线相应的属性,完成注册中断前的设置 • 参数: • irq:需要设置的中断号。可以自定义数值,也可以直接使用kernel/arch/arm/kernel/irqs.h中预定义的值。例如: IRQ_EINT0, IRQ_EINT4_7,IRQ_DMA0等 • edge:中断触发方式,可以是EXT_LOWLEVEL,EXT_HIGHLEVEL,EXT_FALLING_EDGE,EXT_RISING_EDGE,EXT_BOTH_EDGES • pullup:是否拉高。可以设为GPIO_PULLUP_EN和 GPIO_PULLUP_DIS方式。当设为GPIO_PULLUP_EN时,在GPIO没有输入输出时,线路保持高电平。否则始终保持低电平。一般需要设置成拉高状态 • void enable_irq(unsigned int irq); • 参数: • irq:set_external_irq中设置的中断线号 • 功能:使能所选的中断线 • void disable_irq(unsigned int irq); • 参数: • 功能:使得所选择的中断线无效 • 和enable_irq配对使用,实现相反功能
中断相关函数说明 • int request_irq(unsigned int irq, void (*handler)(int,void *,struct pt_regs *), unsigned long irq_flags,const char * devname, void * dev_id); • 功能:定位中断源,使能中断线和IRQ处理函数 • irq:外设所使用的中断号 • handler函数指针:设备驱动程序所实现的ISR函数 • irqflags:中断请求类型标志,可以是下列值的“或”: • SA_SHIRQ:中断是共享的。此时要求参数dev_id必须有效,不能为NULL; IRQ号参数irq不能大于NR_IRQS;且handler指针不能为NULL。否则将出现错误号为-22的参数无效错,无法注册中断服务程序 • SA_INTERRUPT:当处理中断时,其他局部中断不可用 • SA_SAMPLE_RANDOM:中断可以作为随机计量单位熵使用 • devname指针:设备名字字符串 • dev_id:指向全局唯一的设备标识ID,void类型的指针,供驱动程序自行解释
中断相关函数说明 • void free_irq(unsigned int irq, void *dev_id); • 功能:释放和中断号绑定的中断处理函数 • 注意:必须和register_irq()配对使用,它的参数必须和register_irq()里注册的参数一致。dev_id不能是中断服务程序地址,否则执行rmmod删除该将出现错误,必须重启系统才能再次insmod该模块 • 示例
功能描述 • 编写简单的虚拟硬件驱动程序并进行调试 • 参考驱动代码框架如下,其中的demo_read,demo_write函数完成驱动的读写接口功能,do_write函数实现将用户写入的数据逆序排列,通过读取函数读取转换后的数据。这里只是演示接口的实现过程和内核驱动对用户的数据的处理。demo_ioctl函数演示ioctl调用接口的实现过程。
static int demo_open(struct inode *inode, struct file *filp) { /* 初始化一些资源,如将缓存区清零等。有些资源在设备未打开即未被 * 使用之前最好不要打开,如中断、时钟等。 */ memset(wbuff, 0, BUFFSIZE); memset(rbuff, 0, BUFFSIZE); count = 0; return 0; } static int demo_close(struct inode *inode, struct file *filp) { return 0; }
static ssize_t demo_read(struct file *filp, char *buf, size_t cnt, loff_t *off) { if(count < cnt) cnt = count; if(cnt) copy_to_user(buf, rbuff, cnt); /*向应用程序传输数据*/ return cnt; } static ssize_t demo_write(struct file *filp, const char *buf, size_t cnt, loff_t *off) { if(cnt > BUFFSIZE) cnt = BUFFSIZE; copy_from_user(wbuff, buf, cnt); /*接受来自应用程序的数据*/ count = cnt; return cnt; }
static int demo_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) { switch(cmd) { default: break; } return 0; }
static struct file_operations demo_fops = { owner: THIS_MODULE, write: demo_write, read: demo_read, ioctl: demo_ioctl, open: demo_open, release: demo_close, };
static int __init demo_init(void) { int result; /* 为设备驱动程序分配资源,比如分配一些内存 *我们分配两小片的缓存区给驱动程序。当向设备写数据时,数据被保存在 *写缓存区(wbuff)中,当从设备读数据时, 数据从读缓存区(rbuff)中读出。驱动程序负责将数据从wbuff写入rbuff。*/ wbuff = kmalloc(BUFFSIZE, GFP_KERNEL); rbuff = kmalloc(BUFFSIZE, GFP_KERNEL); /* 注册设备 */ result = register_chrdev(DEMO_MAJOR,, “demo”, &demo_fops); if(result < 0) { /* 如果设备注册出错,打印错误提示,返回错误代码 */ printk(KERN_ERR “Cannot register demo device.”); return -EIO; } return 0; /* 一般返回0表示没有错误 */ }
static void __exit demo_exit(void) { int result; /*移除设备*/ result = unregister_chrdev(DEMO_MAJOR, "demo"); if(result < 0) { printk(KERN_ERR "Cannot remove demo device.\n"); return; } /*释放存储区*/ if(wbuff) kfree(wbuff); if(rbuff) kfree(rbuff); return; } MODULE_LICENSE("GPL"); module_init(demo_init); /*标记模块初始化函数*/ module_exit(demo_exit); /*标记模块清除函数*/
用户测试程序test_demo.c • 编写test_demo.c测试驱动程序 • 参见实验指导书
实验步骤 • 参见实验指导书
设备文件系统devfs • 2.4版本Linux内核中引入了设备文件系统Device Filesystem(devfs),所有的设备文件作为一个可以挂装的文件系统,这样就可以被文件系统进行统一管理,从而设备文件就可以挂装到任何需要的地方。命名规则也发生了变化,一般将主设备建立一个目录,再将具体的子设备文件建立在此目录下。比如在UP-NETARM2410-S中的MTD 设备为:/dev/mtdblock/0。使用devfs要求内核编译时定义了符号CONFIG_DEVFS_FS
与devfs有关的函数 devfs_register函数用于向内核注册设备文件,其原型为: devfs_handle_t devfs_register(devfs_handle_t dir, const char *name, unsigned int flags,unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info) devfs_mk_dir函数用于将设备文件创建在一个特定的目录内,其原型: devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const char *name, void *info) devfs_unregister函数用于移除设备文件,其原型为: void devfs_unregister(devfs_handle_t deventry);
参数说明 • dir新创建的设备文件的父目录,一般设为null, 表示父目录为/dev • name设备名称,如想包含子目录,可以直接在名字中包含’/’ • flagsdevfs标志的位掩码。 • major主设备号如果在flags参数中指定为DEVFS_FL_AUTO_DEVNUM,则主次设备号就无用了。 • minor次设备号mode设备的访问模式 • ops设备的文件操作数据结构指针 • infofilp->private_data的默认值。 • deventry指向devfs_register调用后获得的devfs入口
下面在驱动程序里使用devfs,请各位老师根据已给出的程序完成使用devfs的demo程序下面在驱动程序里使用devfs,请各位老师根据已给出的程序完成使用devfs的demo程序
……………………………………………………………………………………………………………… #ifdef CONFIG_DEVFS_FS static devfs_handle_t devfs_demo_dir, devfs_demoraw; #endif ……………………………………………………… static int __init demo_init(void) { /*other code here*/ #ifdef CONFIG_DEVFS_FS devfs_demo_dir = devfs_mk_dir(NULL, "demo", NULL); devfs_demoraw = devfs_register(devfs_demo_dir, "0", DEVFS_FL_DEFAULT, DEMO_MAJOR, DEMO_MINOR, S_IFCHR | S_IRUSR | S_IWUSR, &demo_fops, NULL); #endif /*other code here*/ } static void __exit demo_exit(void) { /*other code here*/ #ifdef CONFIG_DEVFS_FS devfs_unregister(devfs_demoraw); devfs_unregister(devfs_demo_dir); #endif /*other code here*/
在驱动模块成功插入后,会在/dev下面建立一个叫做demo的设备文件,若使用devfs,则无需建立设备节点,插入模块后系统会自动建立/dev/demo/0文件节点。在驱动模块成功插入后,会在/dev下面建立一个叫做demo的设备文件,若使用devfs,则无需建立设备节点,插入模块后系统会自动建立/dev/demo/0文件节点。