您的位置:首页 > 运维架构 > Linux

基于Linux操作系统的底层驱动技术

2010-01-09 14:44 429 查看

这里的底层驱动是指Linux下的底层设备驱动,这些驱动通常都是加载在内核态的,可以提供给上层用户态的应用程序访问底层设备的能力。也就是说,上层应用程序通过底层的驱动程序可以实现输入/输出的管理等功能。

3.1 设备驱动概述

设备管理即输入/输出子系统,可分为上下两部分:一部分是上层的,与设备无关,这部分根据输入/输出请求,通过特定的设备驱动程序接口来与设备进行通信。另一部分是下层的,与设备有关,常称为设备驱动程序,它直接与相应设备打交道,并且向上层提供一组访问接口。
设备管理的目标是对所有外接设备进行良好的读、写、控制等操作。由于用户希望能用同样的应用程序和命令来访问设备和普通文件。为此,Linux中的设备管理应用了设备文件这个概念来统一设备的访问接口。简单地说,系统试图使它对所有各类设备的输入、输出看起来就好像对普通文件的输入、输出一样。用户希望能用同样的应用程序和命令来访问设备和普通文件。
由于Linux中将设备当做文件来处理,所以对设备进行操作的系统调用和对文件的操作类似,主要包括open()、read()、write()、ioctl()、close()等。应用程序发出系统调用指令
以后,会从用户态转换到内核态,通过内核将open()这样的系统调用转换成对物理设备的操作。
Linux下的设备驱动任务包括以下两个。
(1)自动配置和初始化子程序:这部分程序仅在初始化的时候被调用一次。
(2)服务于I/O请求的子程序:这部分是系统调用的结果。在执行这部分程序的时候,系统仍认为和进行调用的进程属于同一个进程,只是由用户态变成了内核态,并具有进行此系统调用的用户程序运行环境,所以可以在其中调用sleep()等与进程运行环境有关的函数。

3.2 设备类型分类

纵览linux/drivers目录,大概还有35个以上的子目录,每个子目录基本上就代表了一种设备驱动,有atm、block、char、misc、input、net、usb、sound、video等。这里只描述在嵌入式系统里面用得最为广泛的3种设备。

1.字符设备(char device)

字符设备是Linux最简单的设备,可以像文件一样访问。初始化字符设备时,它的设备驱动程序向Linux登记,并在字符设备向量表中增加一个device_struct数据结构条目,这个设备的主设备标识符用做这个向量表的索引。一个设备的主设备标识符是固定的。chrdevs向量表中的每一个条目,一个device_struct数据结构,包括两个元素:一个登记设备驱动程序名称的指针和一个指向一组文件操作的指针。可以参考的代码是include/linux/ major.h。
一般来说像鼠标、串口、键盘等设备都属于字符设备。

2.块设备(block device)

块设备是文件系统的物质基础,它也可以像文件一样被访问。Linux用blkdevs向量表维护已经登记的块设备文件。它像chrdevs向量表一样,使用设备的主设备号作为索引。它的条目也是device_struct数据结构。与字符设备不同的是,块设备分为SCSI类和IDE类。向Linux内核登记并向核心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。可以参考的代码是fs/devices.c。
每一个块设备驱动程序必须提供普通的文件操作接口和对于buffer cache的接口。每一个块设备驱动程序填充blk_dev向量表中的blk_dev_struct数据结构。此向量表的索引是设备的主设备号。其中blk_dev_struct数据结构包括一个请求例程的地址和一个指针,指向一个request数据结构的列表,每一个都表达buffer cache向设备读/写一块数据的一个请求。
可以参考的源代码是drivers/block/ll_rw_blk.c和include/linux/blkdev.h。
当buffer cache从一个已登记的设备读/写一块数据,或者希望读、写一块数据到其他
位置时,就在blk_dev_struct中增加一个request数据结构。每个request数据结构都有一个指向一个或多个buffer_head数据结构的指针,每一个都是读/写一块数据的请求。如果buffer_head数据结构被锁定(buffer_cache),可能会有一个进程在等待这个缓冲区的阻塞进程完成。每一个request数据结构都是从all_request表中分配的。如果request增加到空的request列表中,就调用驱动程序的request函数处理这个request队列,否则驱动程序只是简单地处理request队列中的每一个请求。
块设备驱动程序和字符设备驱动程序的主要区别是:在对字符设备发出读、写请求时,实际的硬件I/O一般紧接着就发生了,块设备则不然,它利用一块系统内存作为缓冲区,当用户进程对设备请求能满足用户的要求时,就返回请求的数据,如果不能就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备的,以免耗费过多的CPU时间来等待。
块设备主要有硬盘、光盘驱动器等。可以查看文件/proc/devices获得。

3.网络设备(net device)

网络设备在系统中的作用类似于一个已挂载的块设备。块设备将自己注册到blk_dev数据及其他内核结构中,然后通过自己的request函数在发生请求时传输和接收数据块,同样网络设备也必须在特定的数据结构中注册自己,以便与外界交换数据包时被调用。网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD UNIX的Socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

4.杂项设备(misc device)

杂项设备也是在嵌入式系统中用得比较多的一种设备驱动,在第11章里面介绍的sub LCD和弦芯片的驱动等都是采用 misc device 的驱动方式实现的。在 Linux 内核的include\linux目录下有Miscdevice.h文件,要把自己定义的misc device从设备定义在这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。

3.3 设备驱动中关键数据结构

1.file_operations数据结构

内核内部通过file结构识别设备,通过file_operations数据结构提供文件系统的入口点
函数。file_operations定义在<linux/fs.h>中的函数指针表。这个结构的每一个成员的名字都对应着一个系统调用。从某种意义上说,写驱动程序的任务之一就是完成file_operations
中的函数指针。如果在2.4版本内核下开发的驱动很可能在2.6版本中无法使用,需要进行移植。通常file_operations提供了包括open()、write()、read()、release()、poll()、ioctl()等文件系统的入口函数。
下面简单描述一下几个重要的入口函数。
1)open():
static int mydriver_open(struct inode *inode, struct file *filp)
当上层对mydriver执行open操作时调用该函数,其中参数inode为设备特殊文件的inode(索引节点)结构指针,参数file是指向这一设备的文件结构指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用MINOR(inode→i - rdev)取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误)等。
2)write():
static ssize_t mydriver_write(struct file *filp, const char *buf, size_t size, loff_t *offp)
当设备特殊文件进行write系统调用时,将调用驱动程序的write()函数,向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。Write函数通常就是把数据从用户空间复制到内核空间,所以在write函数里经常会看到copy_from_user()函数。
3)read():
static ssize_t mydriver_read(struct file *filp, char *buf, size_t size, loff_t *offp)
当对设备特殊文件进行read系统调用时,将调用驱动程序read()函数,用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL(“Invalid argument,非法参数”)。函数返回非负值表示成功读取的字节数(返回值为“signed size”数据类型,通常就是目标平台上的固有整数类型)。Read()函数则通常是把数据从内核空间复制到用户空间,一般都会调用copy_to_user()函数。
4)release():
static int mydriver_release(struct inode *inode, struct file *filp)
当最后一个打开设备的用户进程执行close()系统调用时,内核将调用驱动程序的release()函数,release()函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义其他标志的复位等。
5)ioctl():
static int mydriver_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,unsigned int参数为设备驱动程序需要执行的命令代码,由用户自定义。unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl入口点,则对任何内核未预先定义的请求,ioctl系统调用将返回错误(-ENOTTY,“No such ioctl fordevice,该设备无此ioctl命令”)。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。
6)poll():
static unsigned int mydriver_poll(struct file *filp, poll_table *wait)
poll方法是poll和select 这两个系统调用的后端实现,用来查询设备是否可读、可写或是否处于某种特殊状态。

2.inode(索引节点)

文件系统处理的文件所需要的信息在inode(索引节点)中。一个filesystem可以粗略地分成inode table与data area两部分。Inode table上有许多的inode,每个inode分别记录一个档案的属性,以及这个档案分布在哪些data block上。inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。inode结构中的静态信息取自物理设备上的文件系统,由文件系统指定的函数填写,它只存在于内存中,可以通过inode缓存访问。虽然每个文件都有相应的inode节点,但是只有在需要的时候,系统才会在内存中为其建立相应的inode数据结构,建立的inode结构将形成一个链表,可以通过遍历这个链表得到所需要的文件节点。

3.file结构

file结构主要用于与文件系统对应的设备驱动程序使用。在Linux里,每一个档案都有一个file结构和inode结构,inode结构是用来让Kernel做管理的,而file结构则是平常对档案读、写或开启,关闭所使用的。当然,从user的观点来看是看不出什么的。比起inode结构,file结构就显得小多了,file结构也是用串行来管理的,f_next会指到下一个file结构,而f_pprev则会指到上一个file结构的地址,f_dentry会记录其inode的dentry地址,f_mode为档案存取种类,f_pos则是目前档案的offset,每次读写都从offset记录的位置开始读写,f_count是此file结构的reference cout,f_flags则是开启此档案的模式,f_reada,f_ramax, f_raend,f_ralen,f_rawin则是控制read ahead的参数,f_owner记录了要接收SIGIO和SIGURG的行程ID或行程群组ID,private_data则是tty driver所使用的字段。

3.4 设备驱动程序模板与实现

Linux下的驱动程序虽然复杂,但是总结下来还是有很多的规律可寻。Linux下的设备驱动开始编程时显得比较容易,可以轻松地开始驱动编写,但是要把驱动写好也的确需要花一定的时间去研究。

1.设备驱动模板

设备驱动模板代码如例程5-4所示。
例程5‑4 Mydriver.c
#include <linux/module.h>
#include <linux/config.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/miscdevice.h>
#include <linux/ioctl.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/smp_lock.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/ioport.h>
#include <linux/slab.h>
#include <asm/hardware.h>
#include <asm/io.h>
#include <asm/arch/irqs.h>
#include <asm/irq.h>
#include <asm/signal.h>
#include <asm/uaccess.h>
/*定义设备的从设备号*/
#define MYDRIVER_MINOR 174
/*定义设备相关数据结构*/
typedef struct _MYDRIVER_DEV
{
spinlock_t dev_lock;
wait_queue_head_t oWait;
int open_count;

}MYDRIVER_DEV, *PMYDRIVER_DEV;
/*定义设备状态数据结构*/
typedef struct _MYDRIVER_DEV_STATS
{
unsigned long rx_intrs;
unsigned long rx_errors;
unsigned long rx_blocks;
unsigned long rx_dropped;
unsigned long tx_intrs;
unsigned long tx_errors;
unsigned long tx_missed;
unsigned long tx_blocks;
unsigned long tx_dropped;
}MYDRIVER_DEV_STATS, * MYDRIVER_DEV_STATS;
unsigned int IntInit=0;
/*定义设备open接口函数*/
static int mydriver_open(struct inode *inode, struct file * filp)
{
int minor;
DBGPRINT("mydriver_open\n");
minor = MINOR(inode->i_rdev);
if ( minor != MYDRIVER_MINOR ) { return -ENODEV; }

#ifdef MODULE
MOD_INC_USE_COUNT; /*打开使用次数累加*/
#endif

mydriver_dev.open_count++;
if ( mydriver_dev.open_count == 1 )
{
DBGPRINT("mydriver_open: first opne\n");
/*第一次打开设备,在这里可以放些设备初始化代码*/
}
return 0;
}
/*定义设备close接口函数*/
static int mydriver_release(struct inode *inode, struct file *filp)
{
DBGPRINT("mydriver_release\n");
mydriver_dev.open_count--;
if ( mydriver_dev.open_count == 0 )
{
DBGPRINT("mydriver_release: last close\n");
/*设备彻底关闭,这里可以放一些使设备休眠,或者poweroff的代码*/
}
#ifdef MODULE
MOD_DEC_USE_COUNT; /*打开次数递减*/
#endif
return 0;
}
/*定义设备read接口函数*/
static ssize_t mydriver_read(struct file *filp, char *buf, size_t size, loff_t *offp)
{
if(size> 8192) size = 8192;
/* copy_to_user()*/
/*copy kernel space to user space. */
/*把数据从内核复制到用户空间的代码,可以根据实际添加*/
return size; /*返回字节数*/
}
/*定义设备write接口函数*/
static ssize_t mydriver_write(struct file *filp, const char *buf, size_t size, loff_t *offp)
{
lock_kernel();
DBGPRINT("mydriver_write\n");
if(size> 8192) size = 8192;
/*copy_from_user()*/
/*copy user space to kernel space. */ /*把数据从用户空间复制到内核空间*/
unlock_kernel();
return size; /*返回字节数*/
}
/*定义设备ioctl接口函数*/
static int mydriver_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret = 0;
DBGPRINT("mydriver_ioctl: cmd: 0x%x\n", cmd);
switch(cmd)
{
case cmd1: /*命令字,注意幻数的使用*/
…..
break;
…..
case cmd3:
…..
break;
default:
DBGPRINT("mydriver_ioctl: bad ioctl cmd\n");
ret = -EINVAL;
break;
}
return ret;
}
/*定义设备select函数接口*/
static unsigned int mydriver_poll(struct file *filp, poll_table *wait)
{
poll_wait(filp,&mydriver_dev.oWait,wait);
if(IntInit)
{
IntInit=0;
return POLLIN|POLLRDNORM; //可以写
}
else { return POLLOUT; //可以读 }
}
/*定义设备的file_operations*/
static struct file_operations mydriver_fops =
{
owner: THIS_MODULE,
open: mydriver_open,
release: mydriver_release,
read: mydriver_read,
write: mydriver_write,
ioctl: mydriver_ioctl,
poll: mydriver_poll,
};
/*定义设备结构体*/
static struct miscdevice mydriver_miscdev =
{
MYDRIVER_MINOR,
" mydriver ",
& mydriver_fops
};
/*定义设备init函数*/
int __init mydriver_init(void)
{
int ret;
DBGPRINT("mydriver_init\n");
ret =misc_register(&mydriver_miscdev); //注意这里调用misc_register()来注册
if ( ret )
{
DBGPRINT("misc_register failed: 0x%x\n", ret);
return ret;
}
memset(&mydriver_dev, 0, sizeof(mydriver_dev));
init_waitqueue_head(&mydriver_dev.oWait);
spin_lock_init(&mydriver_dev->dev_lock);
/*这里可以放一些硬件初始化的函数*/
return 0;
}
/*定义设备exit函数*/
void __exit mydriver_exit(void)
{
DBGPRINT("mydriver_exit\n");
misc_deregister(&mydriver_miscdev); //注销misc dev
}
module_init(mydriver_init);
module_exit(mydriver_exit);
MODULE_LICENSE("GPL");
从上面的模板代码可以看出,设备驱动主要给上层提供file_operation和ioctl功能,实现上层对于底层设备的管理和读、写操作等。另外不同的设备调用的设备注册和注销函数有所不同,大家可以区分一下:misc_register()函数、register_chardev()函数、register_netdev()函数及misc_deregister()函数。也可以去分析一下deregister_chardev()函数和deregister_netdev() 函数的不同之处。
通常的设备驱动参照上面的模板就可以实现基本的框架了,当然还需要注意有关硬件的一些操作,包括初始化、参数设置、中断服务等。这些代码可以根据系统的设计放在driver_init里面,或者放在第一次打开的时候。

2.设备驱动程序中的中断

在设备驱动程序中通用的申请中断的方法如下:
request_irq(INT_DEVICE, device_intr, SA_INTERRUPT, "device_INT", &my- driver_dev)
n INT_DEVICE:对应的硬件中断号;
n device_intr:中断服务回调函数名;
n SA_INTERRUPT:申请中断的方式,表明这是一个快速中断;
n device_INT:中断的名字;
n mydriver_dev:申请中断的设备。







INT_DEVICE可以定义在include/asm-arm/arch目录下的irqs.h文件里面。另外中断的名字device_INT在什么地方可以看到呢?不妨可以在嵌入式系统启动之后,cat /proc/interrupts看看打印出来的中断信息列表里有没有定义的device_INT这个名字。
request_irq()函数的实体如下:
int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),
unsigned long irq_flags, const char * devname, void *dev_id)
{
unsigned long retval;
struct irqaction *action;
if (irq >= NR_IRQS || !irq_desc[irq].valid || !handler ||
(irq_flags & SA_SHIRQ && !dev_id))
return -EINVAL;
action = (struct irqaction *)kmalloc(sizeof(struct irqaction), GFP_ KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->flags = irq_flags;
action->mask = 0;
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
retval = setup_arm_irq(irq, action);
if (retval)
kfree(action);
return retval;
}
从上面request_irq的原形函数,可以看出其内部其实是调用了setup_arm_irq()函数在系统中注册中断的。当然,如果在module_init函数或者设备对应的open()函数里面申请了中断,那么相应的就应该在module_exit函数或者module_release函数里面调用free_irq()函数来注销中断服务,方法是:
free_irq(INT_DEVICE, &mydriver_dev);
另外,在处理中断的时候尽量用一些内核提供的像cli()、sti()这样的函数。

3.利用ioctl进行设备管理

对于底层设备有的时候需要改变设备的运行状况,有时候需要改变设备的运行参数等。为完成这些参数的设置,上层只要传递少量的命令字或参数给底层设备。对于这样的应用,底层驱动通常是通过给上层提供ioctl函数来实现的。下面先给出一个简单的ioctl例子:
#define MYDRIVER_MINOR 174
#define MYDRIVER_IOC_MAGIC 'm' /*定义幻数*/
#define MYDRIVER_IOC_BASE 0
/*定义命令字*/
#define MYDRIVER_IOC_ON
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 1)
#define MYDRIVER_IOC_OFF
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 2)
#defineMYDRIVER_IOC_SLEEP_IN
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 3)
#define MYDRIVER_IOC_SLEEP_OUT
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 4)
#define MYDRIVER_IOC_RESET
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 5)
#define MYDRIVER_IOC_CLEAR_SCREEN
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 6)
#define MYDRIVER_IOC_CONTRAST_INC
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 7)
#define MYDRIVER_IOC_CONTRAST_DEC
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 8)
#define MYDRIVER_IOC_INIT
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 9)
static int mydriver_ioctl(struct inode *inode, struct file *filp, unsigned
intcmd, unsigned long arg)
{
int ret = 0;
DBGPRINT("mydriver_ioctl: cmd: 0x%x\n", cmd);
/*根据命令执行对应的操作*/
switch(cmd)
{
case MYDRIVER_IOC_RESET:
mydriver_reset();
break;

case MYDRIVER_IOC_ON:
mydriver_on();
break;
case MYDRIVER_IOC_OFF:
mydriver_off();
break;
case MYDRIVER_IOC_SLEEP_IN:
mydriver_sleep_in();
break;

case MYDRIVER_IOC_SLEEP_OUT:
mydriver_sleep_out();
break;

case MYDRIVER_IOC_CLEAR_SCREEN:
mydriver_clear_screen();
break;

case MYDRIVER_IOC_CONTRAST_DEC:
mydriver_contrast_dec();
break;

case MYDRIVER_IOC_CONTRAST_INC:
mydriver_contrast_inc();
break;

case MYDRIVER_IOC_INIT:
mydriver_initial();
break;

default:
DBGPRINT("mydriver_ioctl: bad ioctl cmd\n");
ret = -EINVAL;
break;
}
return ret;
}
特殊的系统函数调用ioctl(input output control)。一般情况下每个设备可以有自己的ioctl命令,它可以读ioctl(从进程向内核发送信息)和写ioctl(返回信息给进程)(注意一下:在此读、写的作用是颠倒的,因此ioctl的读是发送消息给内核而写是从内核接收消息)或什么也不做。ioctl使用3个参数调用:合适的设备文件的文件描述符,ioctl号及一个参数,该参数是类型长度,因此可以使用一个模型传递一些参数。
ioctl号用主设备号,ioctl类型,命令和参数类型编码。这个ioctl号通常用一个头文件中的宏调用(_IO,_IOR,_IOW或_IOWR——取决于类型)创建。如果想在自己的模块中使用ioctl,最好接受官方的ioctl分配。

4.通过proc fs获取设备状态

驱动程序加载(insmod)之后,通过什么样的手段来观测设备的运行状况呢?通常可以在file_operation对应的各个函数里面用printk(内核态常用的打印函数)打印出需要了解的调试信息。如果仔细留意的话可以发现在嵌入式系统的文件系统目录下通常会有proc目录,在该目录下可以通过cat interrupt去了解ARM嵌入式系统中ARM处理器中断的情况,通过cat devices可以了解device设备的状况。那么设备的运行状态是不是也可以通过
proc来了解呢?答案是肯定的。因为proc也是一种Linux下用得比较多的用户态与内核态数据交换的方式,内核的很多数据都是通过这种方式给用户的,内核的很多参数也可以通过这种方式来让用户方便设置的。除了sysctl出口到/proc下的参数,proc提供的大部分内核参数是只读的。实际上,很多应用严重地依赖于proc,因此它几乎是必不可少的组件。那么如何来使用proc呢?首先一定要包含 procfs的头文件,如#include <linux/proc _fs.h>,其次利用procfs提供的如下API函数。
1)struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent)
该函数用于创建一个正常的proc条目,参数name给出了要建立proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数parent指定建立的proc条目所在的目录。如果要在/proc下建立proc条目,parent应当为NULL,否则它应当为proc_mkdir。返回struct proc_dir_entry结构的指针。
2)extern void remove_proc_entry(const char *name, struct proc_dir_entry *parent)
该函数用于删除上面函数创建的proc条目,参数name给出要删除的proc条目的名称,参数parent指定建立的proc条目所在的目录。
3)struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir _entry *parent)
该函数用于创建一个proc目录,参数name指定要创建的proc目录的名称,参数parent为该proc目录所在的目录。
4)extern struct proc_dir_entry *proc_mkdir_mode(const char *name, mode _t mode, struct- proc_dir_entry *parent)
该函数用于以一定的模式创建proc目录,参数name指定要创建的proc目录的名称,参数mode给出了建立的该proc目录的访问权限,参数parent为该proc目录所在的目录。
5)struct proc_dir_entry *proc_symlink(const char * name,struct proc_ dir_entry * parent, const char * dest)
该函数用于建立一个proc条目的符号链接,参数name给出要建立的符号链接proc条目的名称,参数parent指定符号链接所在的目录,参数dest指定链接到的proc条目名称。
6)struct proc_dir_entry *create_proc_read_entry(const char *name,mode_t mode, struct proc_dir_entry *base,read_proc_t *read_proc, void * data)
该函数用于建立一个规则的只读proc条目,参数name给出要建立proc条目的名称,参数mode给出了建立该proc条目的访问权限,参数base指定建立proc条目所在的目录,参数read_proc给出读取该proc条目的操作函数,参数data为该proc条目的专用数据,它将保存在该proc条目对应的struct file结构的private_data字段中。
7)struct proc_dir_entry *create_proc_info_entry(const char *name,mode_t mode, struct proc_dir_entry *base, get_info_t *get_info)
该函数用于创建一个info型的proc条目,参数name给出了要建立proc条目的名称,参数mode给出了建立该proc条目的访问权限,参数base指定建立proc条目所在的目录,参数get_info指定该proc条目的get_info操作函数。实际上get_info等同于read_proc,如果proc条目没有定义read_proc,对该proc条目的read操作将使用get_info取代,因此它在功能上非常类似于函数create_proc_read_entry。
8)struct proc_dir_entry *proc_net_create(const char *name, mode_t mode, get_info_t *get_info)
该函数用于在/proc/net目录下创建一个proc条目,参数name给出了要建立proc条目的名称,参数mode给出了建立该proc条目的访问权限,参数get_info指定该proc条目的get_info操作函数。
9)struct proc_dir_entry *proc_net_fops_create(const char *name, mode_t mode, struct file_operations *fops)
该函数也用于在/proc/net下创建proc条目,但是它也同时指定了对该proc条目的文件操作函数。
10)void proc_net_remove(const char *name)
该函数用于删除前面两个函数在/proc/net目录下创建的proc条目。参数name指定要删除的proc名称。
除了上述这些函数,值得一提的是结构struct proc_dir_entry,为了创建可读可写的proc条目并指定该proc条目的写操作函数,必须设置上面那些创建proc条目的函数返回指针指向的struct proc_dir_entry结构的write_proc字段,并指定该proc条目的访问权限有写权限。
读者可以通过cat和echo等文件操作函数来查看和设置这些proc文件。下面就在上面的mydriver.c里面增加一个可读的proc接口功能,来获取设备的运行状态。
注意要先在头文件引用部分添加#include <linux/proc_fs.h>,
接着在mydriver_init(void)里面添加如下一句语句:
create_proc_read_entry("mydriver", 0, NULL, mydriver_stats, &mydriver_dev);
不要忘记在mydriver_exit(void)里面要添加:
remove_proc_entry("mydriver", NULL);
当然最重要的还是要定读取proc时的操作函数mydriver_stats,具体定义如下:
static int mydriver_stats(char *buf, char **start, off_t offset, int count, int *eof, void *data)
{
PMYDRIVER_DEV dev = (PMYDRIVER_DEV)data;
int len;
char * p = buf;
p += sprintf(p, "mydriver power count: %d\n", dev->power_ count);
p += sprintf(p, "mydriver RX stats: \n");
p += sprintf(p, " intrs: %ld\n", dev->stats.rx_intrs);
p += sprintf(p, " errors: %ld\n", dev->stats.rx_errors);
p += sprintf(p, " blocks: %ld\n", dev->stats.rx_blocks);
p += sprintf(p, " dropped: %ld\n", dev->stats.rx_dropped);
p += sprintf(p, "mydriver TX stats: \n");
p += sprintf(p, " intrs: %ld\n", dev->stats.tx_intrs);
p += sprintf(p, " errors: %ld\n", dev->stats.tx_errors);
p += sprintf(p, " missed: %ld\n", dev->stats.tx_missed);
p += sprintf(p, " blocks: %ld\n", dev->stats.tx_blocks);
p += sprintf(p, " dropped: %ld\n", dev->stats.tx_dropped);
len = p - buf;
return len;
}
通过上面对Linux下驱动的几方面介绍,读者应该可以实现基本的驱动框架,尝试在驱动中处理中断。还可以利用ioctl来进行设备的管理及使用procfs来获取设备的运行状态等。

3.5 设备驱动程序的使用

Linux 2.4的内核下驱动编译出来的名字通常是*.o的文件,而在Linux 2.6的内核下编译出来的文件是*.ko的文件。在编译驱动的时候,注意要在编译的时候加__DKERNEL__和_DMODULE参数。还要注意在 makefile 文件里面正确地指定 KERNELDIR 和INCLUDEDIR。驱动程序有两种加载方式,内核自动加载和手动加载。通常的做法是在调试过程中采用手动加载的方式,等调试好了之后,就可以编译到内核里面采用自动加载的方式。驱动相对于内核来说就是内核的模块。
内核驱动模块的加载命令用insmod,如insmod mydriver.o。这个命令其实就是调用驱动里面的mydriver_init()函数。用insmod命令将编译好的模块调入内存时,就是向系统的字符设备表登记了一个字符设备。如果登记成功,返回设备的主设备号,不成功,返回一个负值。那么内核驱动模块的卸载就是调用rmmod mydriver,这是调用驱动里面的mydriver_exit()函数,它释放字符设备test在系统字符设备表中占有的表项。当然,上层如果要访问内核的驱动模块,还需要在dev目录下添加设备访问节点,在dev目录下执行mknod c主设备号从设备号:
mknod mydriver c 10 174
从设备号可以从0-254,主设备号在include/linux/major.h文件里可以看到具体的定义,在include/linux/miscdevices.h文件里可以看到从设备号的一些定义,在定义自己的设备号的时候注意不要和系统内核中原有的设备号冲突。
另外一点需要注意如果使用了devfs文件系统的话,设备节点的目录是不同的。设备的访问节点要改成/dev/misc/mydriver。可以通过查看编译出来的 system.map 文件或 cat /proc/ksyms查看底层驱动导出的函数。
当内核加载了驱动之后,上层就可以通过驱动对底层设备进行操作了。如例程5-5所示的代码是一个简单的对mydriver进行读写和ioctl的例子。
例程5‑5 test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#if 0
#define DEV_MYDRIVER "/dev/misc/mydriver"
#else
#define DEV_MYDRIVER "/dev/mydriver"
#endif
#define MYDRIVER_IOC_RESET
_IO(MYDRIVER_IOC_MAGIC, MYDRIVER_IOC_BASE + 5)
int dev_cmd(const char * dev, int cmd, unsigned long arg)
{
int fd;
fd = open(dev, O_RDWR);
if ( fd < 0 )
{
perror(dev);
return -1;
}
ioctl(fd, cmd, arg);
close(fd);
return 0;
}
void test_ioctl(void)
{
dev_cmd(DEV_MYDRIVER,MYDRIVER_IOC_RESET,0);
}
void test_mydriver(void)
{
int i, j;
int fd;
char buf[34];
i = 0;
while ( 1 )
{
i++;
printf("\n***** count: %d *****\n", i);
fd = open(DEV_MYDRIVER, O_RDWR);
for ( j = 0; j < 50; j++ )
{
read(fd, buf, sizeof(buf));
write(fd, buf, sizeof(buf));
}
close(fd);
}
}
int main(int argc, char * argv[])
{
if ( argc < 2 )
{
printf("test <a|c>\n");
return 1;
}
switch ( argv[1][0] )
{
case 'a':
test_mydriver();
break;
case 'c':
test_ioctl ();
break;
}
return 0;
}

4 本章小结

本章重点描述了基于ARM平台的驱动开发技术,也适当地介绍了一些有关ARM汇编的基本知识,读者可以看一些有关ARM汇编的书籍做更为详细的了解。对于ARM底层驱动开发,这里也介绍了一些简单的方法与个人的实际体会。通过本章的介绍,读者应该可以轻松开始Linux下的驱动开发了,但是要深入了解Linux下的驱动开发技术,可以看一下《Linux驱动开发技术》一书。在实际的驱动开发中,建议读者先仔细阅读器件的数据手册及针对该器件手册的勘误手册,这能够少走弯路;其次在器件厂家或者网络上查找有没有针对该器件的驱动。原则上不要自己从头开始编写所有的驱动,毕竟是站在巨人的肩膀上可以比巨人看得更远。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: