460 likes | 703 Views
Linux 字符设备驱动程序设计. linux 的驱动程序. Linux 下对外设的访问只能通过驱动程序 Linux 对于驱动程序有统一的接口,以文件的形式定义系统的驱动程序: Open 、 Release 、 read 、 write 、 ioctl … 驱动程序是内核的一部分,可以使用中断、 DMA 等操作 驱动程序需要在用户态和内核态之间传递数据 uClinux 下可以在应用层直接访问外设,操作寄存器口,但是无法处理中断 —— 不推荐使用 对于复杂的应用可以考虑是用 mmap. 内核功能的划分. 进程管理(进程之间的通讯与同步)
E N D
linux的驱动程序 • Linux下对外设的访问只能通过驱动程序 • Linux对于驱动程序有统一的接口,以文件的形式定义系统的驱动程序: • Open、Release、read、write、ioctl… • 驱动程序是内核的一部分,可以使用中断、DMA等操作 • 驱动程序需要在用户态和内核态之间传递数据 • uClinux下可以在应用层直接访问外设,操作寄存器口,但是无法处理中断——不推荐使用 • 对于复杂的应用可以考虑是用mmap
内核功能的划分 • 进程管理(进程之间的通讯与同步) • 内存管理(malloc/free) • 文件系统 • 设备控制 • 网络功能(网络通讯协议等)
Linux下设备和模块的分类 按照上述系统内核的功能,Linux中把系统的设备定义成如下三类: • 字符设备 • 块设备 • 网络设备
Linux下的设备 • Linux的设备以文件的形式存在于/dev目录下 • 设备文件是特殊文件,使用ls /dev -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
主设备号和次设备号 • 主设备号标识设备对应的驱动程序 • 一个驱动程序可以控制若干个设备,次设备号提供了一种区分它们的方法 • 系统增加一个驱动程序就要赋予它一个主设备号。这一赋值过程在驱动程序的初始化过程中 int register_chrdev(unsigned int major, const char*name,struct file_operations *fops);
创建设备节点 • 设备已经注册到内核表中,对于设备的访问通过设备文件(设备文件与设备驱动程序的主设备号匹配),内核会调用驱动程序中的正确函数 • 给程序一个它们可以请求设备驱动程序的名字。这个名字必须插入到/dev目录中,并与驱动程序的主设备号和次设备号相连 • 使用mknod在文件系统上创建一个设备节点 mknod /dev/mydevice c 254 0
动态分配设备号 • 在Documentation/device.txt文件中可以找到已经静态分配给大部分设备的列表 • 由于许多数字已经分配了,为新设备选择一个唯一的号码是很困难的. • 如果调用register_chrdev时的major为零,函数就会选择一个空闲号码并做为返回值返回
动态分配的问题 动态分配的主设备号不能保证总是一样的,无法事先创建设备节点 • 可以从/proc/devices读取 cat /proc/devices • 利用脚本动态创建设备文件节点
设备管理的问题 如今,Linux 支持很多不同种类的硬件。这意味着/dev中都有数百个特殊文件来表示所有这些设备。而且,这些特殊文件中大多数甚至不会映射到系统中存在的设备上
使用devfs • 在Linux 2.4的内核里引入了devfs来解决linux下设备文件管理的问题 • 在驱动程序中通过devfs_register()函数创建设备文件系统的节点 • 系统启动的时候mount设备文件系统 • 所有需要的设备节点都由内核自动管理。/dev目录下只有挂载的设备
Linux 2.6内核与devfs • Linux 2.6内核引入了sysfs文件系统为每个系统的硬件树进行分级处理 • Devfs在Linux 2.6中被标记为舍弃的特性(在Linux 2.6.15及以后的版本则取消了对它的支持 ),而使用udev。 • 维护动态设备 • 从sysfs获得的信息,可以提供对特定设备的固定设备名。对于热插拔的设备,这尤其重要 • udev 是在用户空间的脚本文件,这很容易被编辑和修改 • 为了保证旧应用程序的兼容性,在嵌入式系统中,是用devfs还是一个好方法。即使在Linux 2.6.15内核以后,也可以通过ndevfs(nano devfs)补丁提供对devfs特性的兼容。
Linux内核硬件驱动标准模板 #include <linux/module.h> #include <linux/config.h> #include <linux/init.h> static int __init name_of_initialization_routine(void) { /* code here */ } static void __exit name_of_cleanup_routine(void) { /* code here */ } module_init(name_of_initialization_routine); module_exit(name_of_cleanup_routine);
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设备驱动程序结构 • 结构体file_operations的定义,在include/linux/fs.h中 • 主要包括:open,close(或者release),read,write,ioctl,poll,mmap等
创建一个字符设备 static int __init pxa_Led_init(void) { int ret, i; Updateled(); ret = register_chrdev(0, DEVICE_NAME, &pxa_fops); if (ret < 0) { printk(DEVICE_NAME " can't get major number\n"); return ret; } LedMajor = ret; #ifdef CONFIG_DEVFS_FS devfs_Led_dir = devfs_mk_dir(NULL, "led", NULL); devfs_Ledraw = devfs_register(devfs_Led_dir, "0", DEVFS_FL_DEFAULT, LedMajor, LedRAW_MINOR, S_IFCHR | S_IRUSR | S_IWUSR, &pxa_fops, NULL); #endif printk(DEVICE_NAME " initialized\n"); return 0; } 创建设备节点 /dev/led/0
其他处理 static void __exit pxa_Led_exit(void) { #ifdef CONFIG_DEVFS_FS devfs_unregister(devfs_Ledraw); devfs_unregister(devfs_Led_dir); #endif unregister_chrdev(LedMajor, DEVICE_NAME); } module_init(pxa_Led_init); module_exit(pxa_Led_exit);
相关操作 static int pxa_Led_open(struct inode *inode, struct file *filp) { MOD_INC_USE_COUNT; DPRINTK("open\n"); return 0; } static struct file_operations pxa_fops = { owner: THIS_MODULE, open: pxa_Led_open, write: pxa_Led_write, release: pxa_Led_release, };
点亮LED static ssize_t pxa_Led_write(struct file *file, const char *buffer, size_t count, loff_t * ppos) { copy_from_user(&ledstatus, buffer, sizeof(ledstatus)); Updateled(); DPRINTK("write: led=0x%x, count=%d\n", ledstatus, count); return sizeof(ledstatus); }
配置和编译脚本 • Linux内核的编译过程,是通过内核源码中根目录和各个子目录中的Makefile分级管理的。 • 其中,根目录的Makefile是最重要的,它可以看成是Makefile最初的入口。 • Make脚本读取.config文件,并根据其信息最终生成vmlinux(elf格式的Linux内核)和modules(模块)。make通过向下递归调用子目录中的Makefile来编译这两个目标。
内核的kbuild脚本 • 把驱动程序放到内核中,在编译内核的时候可以自由裁减。 • kbuild脚本随着Linux内核的发展,更新很快 • 参考Documentation/kbuild目录下的相关文档
ARM的工作模式 • ARM处理器有7种操作模式: • 用户模式(usr) - 正常的程序执行模式 • 快速中断模式(fiq)- 支持高速数据传输或通道处理 • 中断模式(irq)- 用于通用中断处理 • 管理员模式(svc)- 操作系统的保护模式. • 中止模式(abt)- 支持虚拟内存和/或内存保护等异常 • 系统模式(sys)- 支持操作系统的特殊用户模式(运行操作系统任务) • 未定义模式(und)- 支持硬件协处理器的软件仿真 • 除了用户模式外,其他模式均可视为特权模式
ARM的寄存器(1) • 37个寄存器 • 31 个通用32位寄存器,包括程序计数器PC • 6 个状态寄存器 • 15个通用寄存器(R0 to R14), 以及2个状态寄存器和程序计数器(PC)在任何时候都中可见的 • 可见的寄存器取决于处理器的模式,不同的模式映射了不同的工作寄存器
ARM的寄存器(2) • R0 到R15 可以直接访问 • R0 到R14 是通用寄存器 • R13: 堆栈指针(sp) (通常) • 每种处理器模式都有单独的堆栈 • R14: 链接寄存器 (lr) • R15:程序计数器(PC) • CPSR – 当前程序状态寄存器,包括代码标志状态和当前模式位 • 5个SPSR--(程序状态保存寄存器) 当异常发生时保存CPSR状态
ARM寄存器的组织 注:表明用户或系统模式使用的正常寄存器已经被异常模式指定的另一个寄存器取代
ARM是如何处理中断的 • 入口是从arch/arm/kernel/entry-armv.S开始 • arch/arm/kernel/traps.c的trap_init函数初始化中断入口 • __trap_init在entry-armv.S中
vector_IRQ的处理过程 • 保存状态 • 预切换模式 • 查表跳转 spsr_cxsf ldr r13, .LCsirq sub lr, lr, #4 str lr, [r13] mrs lr, spsr str lr, [r13, #4] and lr, lr, #15 ldr lr, [pc, lr, lsl #2] movs pc, lr … mrs r13, cpsr bic r13, r13, #MODE_MASK orr r13, r13, #MODE_SVC msr spsr_cxsf, r13
IRQ模式中断的处理 主要分为两种情况: • 从User模式进入IRQ模式 • 从SVC模式进入IRQ模式 其他特权模式均不可能产生IRQ中断,需要有错处处理代码__irq_invalid
__irq_svc的处理过程 svc_r13 R0-R12 低 svc_lr R4 -> svc_r13 堆栈增长方向 lr_irq-4 __temp_irq svc_lr spsr_irq lr_irq-4 … spsr_irq … 高 ldr r7, .LCirq stmia sp, {r0 - r12} add r4, sp, #S_SP sub sp, sp, #S_FRAME_SIZE mov r6, lr stmia r4, {r5, r6, r7, r8, r9} 进入 add r5, sp, #S_FRAME_SIZE ldmia r7, {r7 - r9}
中断处理的流程 • 进入__irq_svc时,为中断返回填充栈内数据 • 占先式(可剥夺)内核的预处理 • get_irqnr_and_base得到中断向量 • 进入C的中断处理函数asm_do_IRQ • 占先式内核的调度处理 • 中断返回
非占先式与占先式 非占先式(non-preemptive) • 非占先式调度法也称作合作型多任务(cooperative multitasking),各个任务彼此合作共享一个CPU。 • 中断服务可以使一个高优先级的任务由挂起状态变为就绪状态。但中断服务以后控制权还是回到原来被中断了的那个任务,直到改任务主动放弃CPU的使用权时,那个高优先级的任务才能获得CPU的使用权。 • 非占先式内核的一个特点是几乎不需要使用信号量保护共享数据。运行着的任务占有CPU,而不必担心被别的任务抢占。 • 非占先式内核的最大缺陷在于其响应高优先级的任务慢,任务已经进入就绪态,但还不能运行,也许要等很长时间,直到当前运行着的任务释放CPU。内核的任务级响应时间是不确定的,不知道什么时候最高优先级的任务才能拿到CPU的控制权,完全取决于应用程序什么时候释放CPU。
低优先级任务 (2) (1) ISR (3) TIME (4) 中断服务程序使 高优先级任务就绪 (5) (6) 高优先级任务 (7) 低优先级任务释放 CPU使用权 非占先式(Non-Preemptive)
占先式(preemptive) • 当系统响应时间很重要时,要使用占先式内核。最高优先级的任务一旦就绪,总能得到CPU的控制权。 • 当一个运行着的任务使一个比它优先级高的任务进入了就绪态,当前任务的CPU使用权就被剥夺了,或者说被挂起了,那个高优先级的任务立刻得到了CPU的控制权。 • 使用占先式内核时,应用程序不应直接使用不可重入型函数。如果调入不可重入型函数时,低优先级的任务CPU的使用权被高优先级任务剥夺,那么,不可重入型函数中的数据就有可能被破坏。
中断服务程序使 高优先级任务就绪 低优先级任务 (2) (1) 高优先级任务 ISR (3) TIME (4) (5) 高优先级任务得到 CPU使用权 (6) 占先式(Preemptive)
低优先级任务 中断处理 时间 高优先级任务 否 是 高优先级 任务就绪 占先式内核的中断处理
asm_do_IRQ中的处理 在arch/arm/kernel/irq.c中 • 从汇编到C传递的参数do_IRQ(int irq, struct pt_regs *regs) • 查表调用注册的中断处理程序。通过全局数组irq_desc管理
注册中断函数 int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irq_flags, const char * devname, void *dev_id) • irq,中断向量号 • handler,中断处理函数 • irq_flags,中断标志: SA_SHIRQ、SA_INTERRUPT、SA_SAMPLE_RANDOM • devname,设备名 • dev_id,设备id,可以为NULL。共享中断必须区分。
注册的中断处理函数handler • 系统所注册的中断处理子程序,中断产生时由系统来调用。原型为: irqreturn_t (*handler)(int irq, void *dev_id, struct pt_regs * regs) • irq,中断号 • dev_id,申请时告诉系统的设备标识 • regs,中断发生时寄存器内容 • 返回值在include/linux/interrupts中定义为typedef int irqreturn_t。非零表示中断有效
ARM Linux 2.4内核中断处理的问题 • 电平中断的处理过程 • 外设芯片产生中断,系统进入do_IRQ函数,并通过 desc->mask_ack清除处理器的中断标志位 • 因为外设产生的中断并没有被处理,其中断输出电平始终有效,中断标志不能被清除 • 在系统调用设备的中断函数进行处理之后,外设中断输出电平才能恢复为无效 • 而此时,处理器对应的中断标志位仍然有效。这将导致此中断第二次进入 • 清除中断标志, 这时可以处理器的清除中断了 • 两次进入中断的影响
Linux 2.6内核的中断管理方法 引入中断管理函数,对中断分类处理: • set_irq_chip,设置芯片的中断管理函数 • set_irq_chained_handler,设置中断处理分支 • set_irq_flags,设置中断标志
重点与作业 • Linux内核按功能可分为哪些模块? • 按照Linux系统内核的功能,Linux中把系统的设备定义哪几类? • ARM处理器有哪几种操作模式?