570 likes | 691 Views
Linux 设备驱动程序. 2014年11月10日. 主要内容. 概述 设备文件接口 中断处理 模块化编程 单按键驱动. 概述. 在一个实用的嵌入式系统中,无论是用于人机交互的基本外设,如触摸屏、小键盘、 LCD 等,还是用来完成其他应用功能的精简板卡都属于输入输出设备。 要有效地管理这些输入输出设备,必须了解 Linux/uClinux 中设备管理的基本原理和实现细节. 概述. Linux 作为 UNIX 的一个变种,继承了 UNIX 的设备管理方法,将所有的设备是具体的文件,通过文件系统层对设备进行访问。
E N D
Linux设备驱动程序 2014年11月10日
主要内容 • 概述 • 设备文件接口 • 中断处理 • 模块化编程 • 单按键驱动
概述 • 在一个实用的嵌入式系统中,无论是用于人机交互的基本外设,如触摸屏、小键盘、LCD等,还是用来完成其他应用功能的精简板卡都属于输入输出设备。 • 要有效地管理这些输入输出设备,必须了解Linux/uClinux中设备管理的基本原理和实现细节
概述 • Linux作为UNIX的一个变种,继承了UNIX的设备管理方法,将所有的设备是具体的文件,通过文件系统层对设备进行访问。 • 这种设备管理方法可以很好地做到“设备无关性”,可以根据硬件外设的更新进行方便的扩展。
设备类型 • Linux中的设备大致可以分为三类: • 字符设备 • 块设备 • 网络设备
字符设备与块设备 • 字符设备没有缓冲区,以字节为单位顺序处理数据,不支持随机读写。常见的字符设备如普通打印机、系统的串口、终端显示器、嵌入式设备中的简单按键、手写板等。 • 块设备是指在输入输出时数据处理以块为单位的设备,一般都采用缓冲技术,支持数据的随机读写。典型的块设备有硬盘、光驱等。
用户进程 文件系统层 设备驱动层 硬件层 字符设备与块设备 • 字符设备和块设备面向的上一层是文件系统层。 • 对用户来说,块设备和字符设备的访问接口都是一组基于文件的系统调用,如read, write等。
网络协议接口层 网络设备接口层 设备驱动功能层 网络设备、介质层 网络设备 • 与块设备和字符设备不同,网络设备面向的上一层是网络协议层。 • 设备文件是一个唯一的名字(如eth0),在文件系统中不存在对应的节点项。 • 内核和网络驱动程序之间的通信使用的是一套和数据包传输相关的函数,而不是read, write等。
设备号 • 每一个设备都有一对主设备号、次设备号的参数作为唯一的标识。 • 主设备号标识设备对应的驱动程序;次设备号用来区分具体驱动程序的实例。 • 主设备号的获取可以通过动态分配或指定的方式。在嵌入式系统中外设较少,一般采用指定的方式。
与设备号相关的宏 #include<linux/kdev_t.h> kdev_t dev; unsigned int ma, mi; • MAJOR(dev) 获取设备dev的主设备号 • MINOR(dev) 获取设备dev的次设备号 • MKDEV(ma,mi) 根据主设备号ma和次设备号mi得到相应的设备dev
添加和注销设备 • 以字符型设备为例 • 添加设备 extern int register_chrdev( unsigned int major, const char *name, struct file_operations *fops);
添加和注销设备 • 注销设备 extern int unregister_chrdev ( unsigned int major, const char *name); 返回
设备文件接口 • 在Linux系统中,对用户程序而言,设备驱动程序隐藏了设备的具体细节,对不同的设备提供一致的接口;一般就是把设备映射成一个特殊的设备文件,用户程序像对普通文件一样对设备文件进行操作; • 在系统内部,设备的存取通过一组固定的入口点来进行,这组入口点由每个设备的设备驱动程序提供。
用户访问接口 • 字符设备驱动程序能够提供的入口点主要有: • open • int open (char *filename, int access); • close • int close (int handle); • read • int read (int handle, void *buf, int count); • write • int write (int handle, void *buf, int count); • ioctl • int ioctl(int fd, int cmd, …);
file结构 • struct file 是一个内核结构,不会出现在用户程序中。它代表一个打开的文件,由内核在open时创建,并传递给在该文件上进行操作的所有函数,直至最后的close函数。
Struct file中重要的字段罗列如下: • Mode_t f_modet 文件模式通过FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写。 • Loff_t f_ops 当前读/写位置。该位只读,不能直接进行操作。 • Unsigned short f_flags 文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC。 • struct inode *f_inode 打开文件所对应的I节点 • struct file_operations *f_op 与文件对应的操作 • void *private_data 系统调用open函数前可将这个指针置为NULL。驱动程序可以将这个字段用于任何目的或者简单忽略这个字段。
file_operations 结构 • 设备驱动程序通过struct file_operations结构向系统说明自身提供的入口点。 • struct file_operations是一组具体操作的集合,包括打开设备、读取设备等。
file_operations 结构原型 • struct file_operations { • loff_t (*llseek) (struct file *, loff_t, int); • ssize_t (*read) (struct file *, char *, size_t,loff_t *); • ssize_t (*write) (struct file *, const char *, size_t,loff_t *); • int (*readdir) (struct file *, void *, filldir_t); • unsigned int (*poll) (struct file *, struct poll_table_struct *); • int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); • int (*mmap) (struct file *, struct vm_area_struct *); • int (*open) (struct inode *, struct file *); • int (*flush) (struct file *); • int (*release) (struct inode *, struct file *); • int (*fsync) (struct inode *, struct dentry *,int); • int (*fasync) (int, struct file *, int); • int (*lock) (struct file *, int, struct file_lock *); • ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); • ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); • struct module *owner; • };
打开与释放设备 • int (* open) (struct inode *, struct file *) • 如果不声明这个操作,系统默认打开永远成功,但不会通知驱动程序。 • int (* release) (struct inode *, struct file *) • file结构被释放时会执行这个操作。注意,并不是进程每次调用close都会执行release,只要file结构被共享,release就会等到所有的副本都关闭之后才会得到调用。
读写设备 • ssize_t (* read) (struct file *, char *, size_t, loff_t *); • ssize_t (* write) (struct file *, const char *, size_t, loff_t *); • 返回值非负代表成功读取或写入的字节数 • 返回值负值代表着发生了错误。具体数值指明了发生何种错误,并在<linux/errno.h>中定义其类型
ioctl • int (* ioctl) (struct inode *, struct file *, unsigned int, unsigned long) • 系统调用ioctl提供一种调用设备特殊命令的操作,它允许应用程序对被驱动硬件的特殊功能进行操作——配置设备以及进入或退出操作模式。而这些控制操作,一般情况下无法通过read/write调用来实现。
poll • unsigned int (* poll) (struct file *, poll_table *); • 非阻塞型I/O的系统调用poll和select 的后端实现。这两个系统调用可用来查询设备是否可读或可写,或是否处于某种特殊状态。 • 如果驱动程序没有定义poll方法,它所驱动的设备就被默认为既可读又可写,且不处于其他的特殊状态。 • 返回值是一个描述设备状态的位掩码。
owner • struct module *owner • 指向“拥有”该结构的模块的指针,内核使用该指针维护模块的使用计数。 • 常见的初始化owner成员(2.4内核以上有效) • owner: THIS_MODULE • 使用宏的初始化方法 • SET_MODULE_OWNER( &xxx_fops );
其他操作 • llseek 修改文件的当前读写位置 • mmap 将设备内存映射到进程地址空间 • flush 在进程关闭设备文件描述符副本的时候,执行设备上尚未完成的操作 • lock 实现文件锁定 • fsync fsync系统调用的后台实现,用户调用它来刷新待处理的数据 • fasync 通知设备,它的FASYNC标志发生变化
file_operations 结构初始化 • file_operations结构初始化方法 • 2.2内核中 static struct file_operations ts_fops = { NULL, /* lseek */ ts_read, /* read */ NULL, /* write */ NULL, /* readdir */ ts_select, /* select */ ts_open, /* open */ ts_release, /* release (close) */ NULL, /* fsync */ ts_fasync /* async notification */ };
2.4内核提供了一种标记化的结构初始化方法 static struct file_operations ts_fops = { owner: THIS_MODULE, read: ts_read, poll: ts_poll, ioctl: ts_ioctl, open: ts_open, release: ts_release, fasync: ts_fasync, }; • 优点:使驱动程序在结构的定义发生变化时更具有可移植性,并且使得代码更加紧凑。它允许对结构成员进行重新排列,在某些场合下,将频繁被访问的成员放在相同的硬件缓存行上,将大大提高性能。
I/O操作 • 阻塞性操作: • 如果进程调用read,但还没有数据,进程必须阻塞。当数据到达时,进程被唤醒,并将数据返回给调用者。 • 如果进程调用write,缓冲区又没有空区,进程也必须阻塞。当数据写出设备后,输出缓冲区中空出部分空间,唤醒进程,write调用即成功完成。 • 非阻塞性操作: • 立即返回。如果进程在没有数据就绪时调用了read,或者在缓冲区没有空间时调用了write,系统简单返回-EAGAIN。
阻塞型I/O • 等待队列:wait_queue_head_t • 进入睡眠状态: • interruptible_sleep_on (wait_queue_head_t *queue); • sleep_on (wait_queue_head_t *queue); • 唤醒进程: • wake_up_interruptible (wait_queue_head_t *queue); • wake_up (wait_queue_head_t *queue); • 两种方式的区别: • sleep_on不能被信号取消,适用于不可中断进程 • interruptible_sleep_on适用于可中断进程(在驱动程序中使用)
非阻塞性I/O • poll和select • 允许进程决定是否可以对一个或多个打开的文件作非阻塞读或写。 • select由BSD UNIX实现,poll由System V实现 • poll提供比select更精确的控制
异步触发 • 打开异步触发的两个步骤: • 使用fcntl系统调用执行F_SETOWN命令,修改file->f_owner为进程ID,让内核知道该通知谁。 • 使用fcntl系统调用执行F_SETFL命令,设置FASYNC标志,驱动程序的fasync方法被调用。 • 当数据到达时,发送一个SIGIO信号给所有注册为异步触发的进程。
int fasync_helper (int fd, struct file *filep, int node, struct fasync_struct **fa); • 当一个打开文件的FASYNC标志被修改时,调用fasync_helper从相关的进程列表中增加或删除文件。 • void kill_fasync (struct fasync_struct **fa, int sig, int band); • 在数据达到时通知所有相关进程。 返回
中断处理 • 中断是发挥硬件性能的一个重要方面。 • Linux系统为中断的管理提供了很好的接口,从应用编程角度来看,编写一个中断处理程序只要根据具体应用实现中断服务子程序,并利用一系列的Linux API函数向内核注册该服务子程序就行了,具体调度处理在Linux内部实现。
安装和释放中断处理函数 int request_irq (unsigned int irq, void (* handler) (int, void *, struct pt_regs *), unsigned long flags, const char *device, void *dev_id); void free_irq (unsigned int irq, void *dev_id);
中断标志 • flag: • SA_INTERRUPT: 快速中断处理程序 • SA_SHIRQ: 中断可在设备间共享 • SA_SAMPLE_RANDOM: 中断时间戳可用来产生系统熵
实现中断处理程序 • 处理程序是在中断期间运行的,因此它的行为受到一些限制。 • 比如处理程序不能向用户空间发送或者接受数据,因为它不是在任何进程的上下文中执行的。 • 处理程序不能做任何可能发生睡眠的操作,例如调用sleep_on。 • 处理程序不能调用schedule函数。
中断号的探测 • 探测中断号: 1)关闭所有的设备中断; 2)sti()打开没有分配的中断号; 3)irqs = probe_irq_on(); 4)要探测的设备产生一个中断; 5)系统得到设备中断; 6)irq = probe_irq_off(irqs)得到设备中断号;
/proc接口 • 当硬件的中断到达处理器时,内部计数会加1,这为检查设备是否按预期工作提供了一种方法。 • 产生的中断报告显示在文件/proc/interrupts中。 返回 Internal 68VZ328 interrupts 1: 4684 timer 2: 860 M68328_UART
模块化编程 • Linux系统中提供了全新的模块化机制:module • 可以根据需要在不重新编译内核的情况下,将编译好的模块动态地插入运行中的内核,或者将内核中已经存在的一个模块移走。
声明这是一个模块 使用内核头文件中被保护的声明 Hello, World #define MODULE #include <linux/module.h> int init_module(void) { printk (“Hello, world \n”); return 0; } void cleanup_module(void) { printk (“Goodbye cruel world\n”); } • 运行结果: root# gcc –D__KERNEL__ -c hello.c root# insmod hello.o Hello, world root# rmmod hello Goodbye cruel world root#
初始化和清除函数 • 旧的内核中用init_module来初始化一个刚刚加载的模块,并在模块即将卸载之前调用cleanup_module函数。 int init_module(void) void cleanup_module(void) • 在较新的内核中可以给这两个函数重新命名。比如将模块初始化函数命名为my_init,将清除函数命名为my_cleanup, 然后用下面两行来进行标识: module_init(my_init); module_exit(my_cleanup);
模块的使用计数 • 为了确定模块是否能够安全卸载,系统为每个模块保留一个使用计数,以此来确定模块是否忙。 • 在较新的内核版本中,系统能够自动跟踪使用“使用计数”,但有时仍需要我们手动调整使用计数。 • 手动操作使用计数时使用的三个宏: • MOD_INC_USE_COUNT 当前模块计数加1 • MOD_DEC_USE_COUNT 当前模块计数减1 • MOD_IN_USE 计数非0时返回真 • 注意在cleanup_module函数内部不需要检查MOD_IN_USE,因为rmmod命令先调用系统调用delete_module,这个系统调用会先检查模块的使用计数。
模块配置参数 • 在旧内核中,没有显式的参数定义方法,insmod可以改变模块中任何变量值 • 2.1.18以后的内核引入了MODULE_PARM宏,使模块参数更加清楚和安全 MODULE_PARM (variable, type) MODULE_PARM_DESC (variable, description) 返回
单按键驱动实例 硬件原理
变量定义 • #define button_major 50 //主设备号50 • DECLEAR_WAIT_QUEUE_HEAD (wq) // 定义等待队列 • static char message; • struct file_operations button_fops= • { • read: button_read, • open: button_open, • release: button_close, • };
int init_button(void) { int rc; rc=register_chrdev (button_major, “button”, &button_fops); if (rc<0) { printk ("Panic! Could not register BUTTON-Driver\n"); return rc; } ICR = 0x0f00; PDDIR = 0x00; PDSEL = 0x00; PDKBEN = 0x00; return 0; } 设备初始化
在open函数中向内核注册中断 • static int button_open(struct inode *inode, struct file *file) • { • int rc; • rc=request_irq (IRQ6_IRQ_NUM, button_interrupt, IRQ_FLG_STD, “button-IRQ”, NULL); • if (rc) { • printk (“button-Driver: Error while installing interrupt handler\n”); • return –ENODEV; • } • return 0; • }
Read的实现 • static ssize_t button_read (struct file *file, char *buff, size_t len, loff_t *offset) • { • interruptible_sleep_on (&wq); • copy_to_user(buff,&message,1); • return 1; • }
release和cleanup的实现 • int button_close(struct inode *, struct file *) • { • free_irq(IRQ6_IRQ_NUM,NULL); • return 0; • } • void cleanup_button() • { • unregister_chrdev(button_major, “button”); • }
中断处理程序 • static void button_interrupt(int irq, void *dev_id, struct pt_regs *regs) • { • ISR |= (1<<18); //屏蔽中断 • message='A'; • wake_up_interruptible (&wq); • return; • }
驱动与系统结合 • 两种方法: • 1、直接编进内核 • 2、采用module方式加载 • #define MODULE • #include <linux/module.h> • module_init(init_button); • module_exit(cleanup_button);