浅析设备驱动程序通知应用程序的几种方法

时间:2022-04-30 01:21:29 其他范文 收藏本文 下载本文

“Christina”为你分享8篇“浅析设备驱动程序通知应用程序的几种方法”,经本站小编整理后发布,但愿对你的工作、学习、生活带来方便。

浅析设备驱动程序通知应用程序的几种方法

篇1:浅析设备驱动程序通知应用程序的几种方法

为了保证操作系统的安全性和稳定性以及应用程序的可移植性,Windows操作系统不允许应用程序直接访问系统的硬件资源,而是必须借助于相应的设备驱动程序,设备驱动程序可以直接操作硬件,如果应用程序和设备驱动程序之间实现了双向通信,也就达到了应用程序控制底层硬件设备的目的。它们之间的通信包括两个方面:一方面是应用程序传送给设备驱动程序的数据;另一方面是设备驱动程序发送给应用程序的消息。前者的实现较容易,通过CreateFile 函数获取设备驱动程序的句柄后,就可以使用Win32函数,如DeviceIoControl()、ReadFile()或WriteFile()等实现应用程序与设备驱动程序之间的通信。后者的实现远比前者复杂,同时介绍这方面情况的文章较少。这不等于说它不重要,相反,它在有些应用场合发挥着重要的作用。设备驱动程序完成数据的采集工作后,需要马上通知应用程序,以便应用程序能够及时将数据取走并进行处理。诸如此类情况,不一而足。

鉴于设备驱动程序通知应用程序的重要性,本人结合一些经验,对它进行了总结,归纳出5种方法:异步过程调用(APC)、事件方式(VxD)、消息方式、异步I/O方式和事件方式(WDM)。下面分别说明这几种方式的原理,并给出实现的部分源代码。

1 异步过程调用(APC)

Win32应用程序使用CreateFile()函数动态加载设备驱动程序,然后定义一个回调函数backFunc(),并且将回调函数的地址&backFunc()作为参数,通过DeviceIoControl()传送给设备驱动程序。设备驱动程序获得回调函数的地址后,将它保存在一个全局变量(如callback)中,同时调用Get_Cur_Thread_Handle()函数获取它的应用程序线程的句柄,并且将该句柄保存在一个全局变量(如appthread)中。当条件成熟时,设备驱动程序调用 _VWIN32_QueueUserApc()函数,向Win32应用程序发送消息。这个函数带有三个参数:第一个参数为回调函数的地址(已经注册);第二个参数为传递给回调函数的消息;第三个参数为调用者的线程句柄(已经注册)。Win32应用程序收到消息后,自动调用回调函数(实际是由设备驱动程序调用)。回调函数的输入参数是由设备驱动程序填入的,回调函数在这里主要是对消息进行处理。

2 事件方式(VxD)

首先,Win32应用程序创建一个事件的句柄,称其为Ring3句柄。由于虚拟设备驱动程序使用事件的Ring0句柄,因此,需要创建Ring0句柄。用 LoadLibrary()函数加载未公开的动态链接库Kernel32.dll,获得动态链接库的句柄。然后,调用GetProcAddress(), 找到函数OpenVxDHandle()在动态链接库中的位置。接着,用OpenVxDHandle()函数将Ring3事件句柄转化为Ring0事件句柄。Win32应用程序用CreateFile()函数加载设备驱动程序。如果加载成功,则调用DeviceIoControl()函数将Ring0事件句柄传给VxD;同时,创建一个辅助线程等待信号变成有信号状态,本身则可去干其它的事情。当条件成熟时,VxD置Ring0事件为有信号状态(调用 _VWIN32_SetWin32Event()函数),这马上触发对应的Ring3事件为有信号状态。一旦Ring3事件句柄为有信号状态,Win32 应用程序的辅助线程就对这个消息进行相应的处理。

3 消息方式

Win32应用程序调用CreateFile()函数动态加载虚拟设备驱动程序。加载成功后,通过调用DeviceIoControl()函数将窗体句柄传送给VxD,VxD利用这个句柄向窗体发消息,

当条件满足时,VxD调用SHELL_PostMessage()函数向Win32应用程序发送消息。要让该函数使用成功,必须用#define来自定义一个消息,并且也要照样在应用程序中定义它;还要在消息循环中使用ON_MESSAGE()来定义消息对应的消息处理函数,以便消息产生时,能够调用消息处理函数。SHELL_PostMessage()函数的第一个参数为Win32窗体句柄,第二个参数为消息ID号,第三、四个参数为发送给消息处理函数的参数,第五、六个参数为回调函数和传给它的参数。Win32应用程序收到消息后,对消息进行处理。

4 异步I/O方式

Win32应用程序首先调用CreateFile()函数加载设备驱动程序。在调用该函数时,将倒数第2个参数设置为 FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,表示以后可以对文件进行重叠I/O操作。当设备驱动程序文件创建成功后,创建一个初始态为无信号、需要手动复位的事件,并且将这个事件传给类型为OVERLAPPED的数据结构(如Overlapped)。然后,将 Overlapped作为一个参数,传给DeviceIoControl()函数。设备驱动程序把这个I/O请求包(IRP)设置为挂起状态,并且设置一个取消例程。如果当前IRP队列为空,则将这个IRP传送给StartIo()例程;否则,将它放到IRP队列中。设备驱动程序做完这些工作后,结束这个 DeviceIoControl()的处理,于是Win32应用程序可能不等待IRP处理完,就从DeviceIoControl()的调用中返回。通过判断返回值,得到IRP的处理情况。如果当前IRP处于挂起状态,则主程序先做一些其它的工作,然后调用WaitForSingleObject()或 WaitForMultipleObject()函数等待Overlapped中的事件成为有信号状态。设备驱动程序在适当的时候处理排队的IRP,处理完成后,调用IoCompleteRequest()函数。该函数将Overlapped中的事件设置为有信号状态。Win32应用程序对这个事件马上进行响应,退出等待状态,并且将事件复位为无信号状态,然后调用GetOverlappedResult()函数获取IRP的处理结果。

5 事件方式(WDM)

Win32应用程序首先创建一个事件,然后将该事件句柄传给设备驱动程序,接着创建一个辅助线程,等待事件的有信号状态,自己则接着干其它事情。设备驱动程序获得该事件的句柄后,将它转换成能够使用的事件指针,并且把它寄存起来,以便后面使用。当条件具备后,设备驱动程序将事件设置为有信号状态,这样应用程序的辅助线程马上知道这个消息,于是进行相应的处理。当设备驱动程序不再使用这个事件时,应该解除该事件的指针。

6 结语

在目前流行的Windows操作系统中,设备驱动程序是操纵硬件的最底层软件接口。它向上提供与硬件无关的用户接口,向下直接进行I/O、硬件中断、 DMA和内存访问等操作。它将应用程序与硬件细节屏蔽开来,使软件不依赖于硬件并且可在多个不同的平台之间移植。本文介绍了5种设备驱动程序通知应用程序的方法,其中前3种方法主要用于VxD中,后2种方法主要用于WDM。这5种方法都经过实际测试。测试结果表明,它们都能够达到设备驱动程序通知应用程序的目的。

参考文献:

[1]欧青立,徐建波,李方敏,等. 虚拟设备驱动程序VxD的研究与开发. 计算机工程,

[2](美)Chris Cant. Windows WDM设备驱动程序开发指南. 孙义, 马莉波, 国雪飞等译. 北京: 机械工业出版社

[3]李和平. 基于DSP的ICT图像重建系统研究. 北京: 北京航空航天大学机械工程及自动化学院,

篇2:linux设备驱动程序的编写

先介绍下我的环境,用的ubuntu发行版,kernel版本是3.8.0,《Linux设备驱动程序》一书是针对2.6.0的kernel,在这个例子中会看到一些差异,这个例子要实现的目标是,编译装载该module后,通过udev动态的生成设备节点star0,读取设备节点cat /dev/star0可以打印出星号,星号的数量可以由模块参数动态的指定。以下为源代码:

star.h

#ifndef STAR_H_H_H

#define STAR_H_H_H

#define AUTHOR “Tao Yang”

#define DESCRIPTION “A CHAR DEVICE DIRVERS SAMPLE USING UDEV”

#define VERSION “0.1”

#endif

star.c

//module_init module_exit

#include

#include

//module_param

#include

//printk container_of

#include

//dev_t MAJOR MINOR MKDEV

#include

//file_operations file register/unregister_chrdev_region alloc_chrdev_region register/unregister_chrdev(old)

#include

//cdev cdev_init/add/del

#include

//copy_from_user copy_to_user

#include

#include

#include “star.h”

//define device number

#define STAR_MAJOR 0

#define STAR_MINOR 0

#define STAR_DEVS 1

#define DEVICE_NAME “star0”

#define CLASS_NAME “star”

static int howmany=5;

module_param(howmany,int,S_IRUGO);

//module info

MODULE_AUTHOR(AUTHOR);

MODULE_DESCRIPTION(DESCRIPTION);

MODULE_VERSION(VERSION);

MODULE_LICENSE(“GPL”);

//device variables

static struct class* star_class=NULL;

static struct device* star_sysdevice=NULL;

int star_major=STAR_MAJOR;

int star_minor=STAR_MINOR;

int star_nr_devs=STAR_DEVS;

//device struct

static struct star_dev {

char *data;

struct cdev cdev;

};

static struct star_dev star_device;

static ssize_t star_read(struct file * filp,char * buf,size_t count,loff_t *ppos)

{

int i;

char star_str[10000];

struct star_dev *dev=filp->private_data;

for (i=0;i< p=“”>

star_str[i]='*';

}

star_str[howmany]='n';

int len=strlen(star_str);

if(count< p=“”>

return -EINVAL;

if(*ppos!=0)

return 0;

if(copy_to_user(buf,star_str,len))

return -EINVAL;

*ppos=len;

return len;

}

int star_open(struct inode *inode,struct file *filp)

{

struct star_dev *dev;

dev=container_of(inode->i_cdev,struct star_dev,cdev);

filp->private_data=dev;

//......

return 0;

}

//file operations

static const struct file_operations star_fops = {

.owner = THIS_MODULE,

.read = star_read,

.open = star_open,

};

static void star_setup_cdev(struct star_dev *dev,int index)

{

int err,devno=MKDEV(star_major,star_minor+index);

printk(KERN_ALERT “setup cdev...n”);

cdev_init(&dev->cdev,&star_fops);

dev->cdev.owner=THIS_MODULE;

dev->cdev.ops=&star_fops;

err=cdev_add(&dev->cdev,devno,1);

if(err)

printk(KERN_ALERT “Error %d adding star%d”,err,index);

}

static int __init star_init(void)

{

int ret;

dev_t dev;

printk(KERN_ALERT “hello tom!n”);

if(star_major){

dev=MKDEV(star_major,star_minor);

ret=register_chrdev_region(dev,star_nr_devs,DEVICE_NAME);

printk(KERN_ALERT “static!n”);

}else{

ret=alloc_chrdev_region(&dev,star_minor,star_nr_devs,DEVICE_NAME);

star_major=MAJOR(dev);

printk(KERN_ALERT “dynamic!n”);

printk(KERN_ALERT “Device Major is %d!n”,star_major);

}

if(ret<0){

printk(KERN_ALERT “star:can't get major %dn”,star_major);

return ret;

}

printk(KERN_ALERT “set up cdev!”);

star_setup_cdev(&star_device,0);

star_class=class_create(THIS_MODULE,CLASS_NAME);

if(IS_ERR(star_class)){

printk(KERN_ALERT “failed to register device class '%s'n”,CLASS_NAME);

}

//with a class ,the easiest way to instantiate a device is to call device_create()

star_sysdevice=device_create(star_class,NULL,MKDEV(star_major,0),NULL,DEVICE_NAME);

return 0;

}

static void __exit star_exit(void)

{

device_destroy(star_class,MKDEV(star_major,star_minor));

class_unregister(star_class);

class_destroy(star_class);

cdev_del(&star_device.cdev);

printk(KERN_ALERT “goodbye...n”);

}

module_init(star_init);

module_exit(star_exit);

Makefile

ifneq ($(KERNELRELEASE),)

obj-m:=star.o

else

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

PWD :=$(shell pwd)

default:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

endif

编译源代码:

root@ubuntu:~/embedded_linux/ldd3/practice# make

make -C /lib/modules/3.8.0-29-generic/build M=/home/tom/embedded_linux/ldd3/practice modules

make[1]: Entering directory `/usr/src/linux-headers-3.8.0-29-generic'

CC [M] /home/tom/embedded_linux/ldd3/practice/star.o

/home/tom/embedded_linux/ldd3/practice/star.c:58:1: warning: useless storage class specifier in empty declaration [enabled by default]

/home/tom/embedded_linux/ldd3/practice/star.c: In function ‘star_read’:

/home/tom/embedded_linux/ldd3/practice/star.c:72:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]

/home/tom/embedded_linux/ldd3/practice/star.c:66:22: warning: unused variable ‘dev’ [-Wunused-variable]

/home/tom/embedded_linux/ldd3/practice/star.c:81:1: warning: the frame. size of 10032 bytes is larger than 1024 bytes [-Wframe-larger-than=]

Building modules, stage 2.

MODPOST 1 modules

CC /home/tom/embedded_linux/ldd3/practice/star.mod.o

LD [M] /home/tom/embedded_linux/ldd3/practice/star.ko

make[1]: Leaving directory `/usr/src/linux-headers-3.8.0-29-generic'

root@ubuntu:~/embedded_linux/ldd3/practice#

安装驱动模块:

root@ubuntu:~/embedded_linux/ldd3/practice# insmod star.ko

root@ubuntu:~/embedded_linux/ldd3/practice# dmesg -c

[ 3946.696548] hello tom!

[ 3946.699892] dynamic!

[ 3946.699894] Device Major is 249!

[ 3946.699895] set up cdev!

篇3:Linux字符设备驱动程序解析

Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得linux的设备操作犹如文件一般,在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作,如open、close()、read()、write() 等。

Linux主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。

下面我们来假设一个非常简单的虚拟字符设备:这个设备中只有一个4个字节的全局变量int global_var,而这个设备的名字叫做“globalvar”。对“globalvar”设备的读写等操作即是对其中全局变量global_var的操作。

驱动程序是内核的一部分,因此我们需要给其添加模块初始化函数,该函数用来完成对所控设备的初始化工作,并调用register_chrdev() 函数注册字符设备:

static int __init globalvar_init(void){ if (register_chrdev(MAJOR_NUM, “ globalvar ”, &gobalvar_fops)) { //…注册失败 } else { //…注册成功 }}

其中,register_chrdev函数中的参数MAJOR_NUM为主设备号, “globalvar”为设备名,globalvar_fops为包含基本函数入口点的结构体,类型为file_operations。当globalvar模块被加载时,globalvar_init被执行,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。

与模块初始化函数对应的就是模块卸载函数,需要调用register_chrdev()的“反函数”

unregister_chrdev():static void __exit globalvar_exit(void){ if (unregister_chrdev(MAJOR_NUM, “ globalvar ”)) { //…卸载失败 } else { //…卸载成功 }}

随着内核不断增加新的功能,file_operations结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open()、release()、read()、write()、ioctl()、llseek()、poll()等。

open()函数 对设备特殊文件进行open()系统调用时,将调用驱动程序的open() 函数:

int (*open)(struct inode * ,struct file *);

其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode-> i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误)等;

release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release() 函数:

void (*release) (struct inode * ,struct file *) ;

release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。

read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read()函数:

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL(“Invalid argument,非法参数”)。函数返回非负值表示成功读取的字节数(返回值为“signed size”数据类型,通常就是目标平台上的固有整数类型)。

globalvar_read函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){ … copy_to_user(buf, &global_var, sizeof(int)); …}

write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。

globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off){…copy_from_user(&global_var, buf, sizeof(int));…}

ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);

unsigned int参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,“No such ioctl fordevice,该设备无此ioctl 命令”)。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。

llseek()函数 该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:

loff_t (*llseek) (struct file *, loff_t, int);

poll()函数 poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:

unsigned int (*poll) (struct file *, struct poll_table_struct *);

我们将在“设备的阻塞与非阻塞操作”一节对该函数进行更深入的介绍,

设备“gobalvar”的驱动程序的这些函数应分别命名为gobalvar_open、 gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl,因此设备“gobalvar”的基本入口点结构变量gobalvar_fops 赋值如下:

struct file_operations gobalvar_fops = { read: gobalvar_read, write: gobalvar_write,};

上述代码中对gobalvar_fops的初始化方法并不是标准C所支持的,属于GNU扩展语法。

完整的globalvar.c文件源代码如下:

#include #include #include #include MODULE_LICENSE(“GPL”);#define MAJOR_NUM 254 //主设备号static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);//初始化字符设备驱动的file_operations结构体struct file_operations globalvar_fops ={ read: globalvar_read, write: globalvar_write,};static int global_var = 0; //“globalvar”设备的全局变量static int __init globalvar_init(void){ int ret; //注册设备驱动 ret = register_chrdev(MAJOR_NUM, “globalvar”, &globalvar_fops); if (ret) { printk(“globalvar register failure”); } else { printk(“globalvar register success”); } return ret;}static void __exit globalvar_exit(void){ int ret; //注销设备驱动 ret = unregister_chrdev(MAJOR_NUM, “globalvar”); if (ret) { printk(“globalvar unregister failure”); } else { printk(“globalvar unregister success”); }}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){ //将global_var从内核空间复制到用户空间 if (copy_to_user(buf, &global_var, sizeof(int))) { return - EFAULT; } return sizeof(int);}static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off){ //将用户空间的数据复制到内核空间的global_var if (copy_from_user(&global_var, buf, sizeof(int))) { return - EFAULT; } return sizeof(int);}module_init(globalvar_init);module_exit(globalvar_exit); 运行:gcc -D__KERNEL__ -DMODULE -DLINUX -I /usr/local/src/linux2.4/include -c -o globalvar.o globalvar.c 编译代码,运行:inmod globalvar.o 加载globalvar模块,再运行:cat /proc/devices 发现其中多出了“254 globalvar”一行,如下图: 接着我们可以运行:mknod /dev/globalvar c 254 0 创建设备节点,用户进程通过/dev/globalvar这个路径就可以访问到这个全局变量虚拟设备了。我们写一个用户态的程序globalvartest.c来验证上述设备:#include #include #include #include main(){ int fd, num; //打开“/dev/globalvar” fd = open(“/dev/globalvar”, O_RDWR, S_IRUSR | S_IWUSR); if (fd != -1 ) { //初次读globalvar read(fd, &num, sizeof(int)); printf(“The globalvar is %dn”, num); //写globalvar printf(“Please input the num written to globalvarn”); scanf(“%d”, &num); write(fd, &num, sizeof(int)); //再次读globalvar read(fd, &num, sizeof(int)); printf(“The globalvar is %dn”, num); //关闭“/dev/globalvar” close(fd); } else { printf(“Device open failuren”); }} 编译上述文件:gcc -o globalvartest.o globalvartest.c 运行./globalvartest.o 可以发现“globalvar”设备可以正确的读写。

篇4:QNX 4.25设备驱动程序的编写

QNX 4.25设备驱动程序的编写

摘要:介绍实时操作系统QNX4.25下编写设备驱动程序的大体框架、底层细节以及诸多注意点。针对使用较为普遍的PCI设备作为较为详细的描述。关键词:驱动程序 QNX 实时操作系统 PCI引言QNX是一个多任务、多用户、分布式、可嵌入式符合POSIX标准的微内核的主流实时操作系统,广泛用于实时性能、开发灵活性、网络灵活性要求较高的场合,如电信系统、医疗仪器、航空航天、工业自动化、交通运输、POS机、信息家电等。QNX是一个适合软件/硬件定制的实时操作系统。如果你曾经试图在传统的UNIX或Windows平台下开发设备驱动程序,那么,QNX下开发驱动程序一定会让你受宠若惊。由于QNX的微内核结构,QNX下的系统进程和用户所写的进程没有什么不同,甚至没有私有的隐藏起来的以至用户不能使用的界面。正是这种结构给QNX带来了无与伦比的可扩展性,使得在QNX下写驱动程序如同写其它程序一般方便。设备驱动程序能够获取普通程序所能获得的任务服务。在QNX中增加一个新的驱动程序不会影响操作系统其它程序的任何部分,QNX环境所需的唯一改变是实现地启动新的驱动程序。当然,我们会遇到形形色色的硬件设备,某些驱动程序可能将以特殊方式控制设备的存在和配置。本文只想集中讨论QNX下如何进入、控制设备级的通用硬件,对所有驱动程序来讲这是一个共性问题。其中,将对使用较多的PCI设备作较为详细的叙述。以下是硬件驱动程序的编写。1 探测硬件首先,需要判断设备是否存在,然后查询该设备的配置(例如,设备基地址、中断号等)。对于某类设备,一般会有一大相应的标准机制来判断其配置。每块设备的基地址、中断号等是编程必须的`资源,例如,常用的ISA及PCI硬件设备。对于ISA设备,一般由板上手工跳线设定,不言自明;对于常用的PCI设备,这些资源会由系统自动分配,特别是添减设备,可能会发生变化。因此,在驱动程序中能够动态查找这些资源显得比较重要。对于诸如A/D、D/A、定时卡、I/O板卡这类设备,对照硬件手册编写一些简单的驱动程序并不困难。如果有DOS下驱动程序的C源码,移值应该更容易一些。为了实现对PCI总线设备的控制和管理,必须访问PCI设备的配置空间。配置空间是一容量为256字节并具特定纪录结构的地址空间。该地址空间的结构如图1所示。NQX4.25pp sys/pci.h中对应的结构体定义。

每个PCI设备具有唯一的厂商标识(vendor id)和设备标识(device id),这些信息由硬件手册提供或系统启动时可以看到。下面一段代码展示了于一个给定的PCI设备如何调用QNX相关的函数、侦测设备的存在以及系统分配的资源。其中,标识(index)用来支持和区分具有同样厂商标识和设备标识的几块同样的设备。Index从0开始,如果指定为1,将标识第二块同型号的设备。本例中,YOUR_PCI_DEVICE_ID、YOUR_PCI)CENDOR)OD值是研华的PCL-1713采集卡,可以根据所使用的硬件填以合适的值。以根据所使用的硬件填以合适的值。#include#include#include#include#include#include#include

[1] [2] [3] [4]

篇5:QNX 4.25设备驱动程序的编写

QNX 4.25设备驱动程序的编写

摘要:介绍实时操作系统QNX4.25下编写设备驱动程序的大体框架、底层细节以及诸多注意点。针对使用较为普遍的PCI设备作为较为详细的描述。关键词:驱动程序 QNX 实时操作系统 PCI引言QNX是一个多任务、多用户、分布式、可嵌入式符合POSIX标准的微内核的主流实时操作系统,广泛用于实时性能、开发灵活性、网络灵活性要求较高的场合,如电信系统、医疗仪器、航空航天、工业自动化、交通运输、POS机、信息家电等。QNX是一个适合软件/硬件定制的实时操作系统。如果你曾经试图在传统的UNIX或Windows平台下开发设备驱动程序,那么,QNX下开发驱动程序一定会让你受宠若惊。由于QNX的微内核结构,QNX下的系统进程和用户所写的进程没有什么不同,甚至没有私有的隐藏起来的以至用户不能使用的界面。正是这种结构给QNX带来了无与伦比的可扩展性,使得在QNX下写驱动程序如同写其它程序一般方便。设备驱动程序能够获取普通程序所能获得的任务服务。在QNX中增加一个新的驱动程序不会影响操作系统其它程序的任何部分,QNX环境所需的唯一改变是实现地启动新的驱动程序。当然,我们会遇到形形色色的硬件设备,某些驱动程序可能将以特殊方式控制设备的存在和配置。本文只想集中讨论QNX下如何进入、控制设备级的通用硬件,对所有驱动程序来讲这是一个共性问题。其中,将对使用较多的PCI设备作较为详细的叙述。以下是硬件驱动程序的编写。1 探测硬件首先,需要判断设备是否存在,然后查询该设备的配置(例如,设备基地址、中断号等)。对于某类设备,一般会有一大相应的标准机制来判断其配置。每块设备的基地址、中断号等是编程必须的资源,例如,常用的ISA及PCI硬件设备。对于ISA设备,一般由板上手工跳线设定,不言自明;对于常用的PCI设备,这些资源会由系统自动分配,特别是添减设备,可能会发生变化。因此,在驱动程序中能够动态查找这些资源显得比较重要。对于诸如A/D、D/A、定时卡、I/O板卡这类设备,对照硬件手册编写一些简单的驱动程序并不困难。如果有DOS下驱动程序的C源码,移值应该更容易一些。为了实现对PCI总线设备的控制和管理,必须访问PCI设备的配置空间。配置空间是一容量为256字节并具特定纪录结构的地址空间。该地址空间的结构如图1所示。NQX4.25pp sys/pci.h中对应的结构体定义。

每个PCI设备具有唯一的厂商标识(vendor id)和设备标识(device id),这些信息由硬件手册提供或系统启动时可以看到。下面一段代码展示了于一个给定的PCI设备如何调用QNX相关的函数、侦测设备的存在以及系统分配的资源。其中,标识(index)用来支持和区分具有同样厂商标识和设备标识的几块同样的设备。Index从0开始,如果指定为1,将标识第二块同型号的设备。本例中,YOUR_PCI_DEVICE_ID、YOUR_PCI)CENDOR)OD值是研华的PCL-1713采集卡,可以根据所使用的硬件填以合适的值。以根据所使用的硬件填以合适的值。#include#include#include#include#include#include#include#include#define YOUR_PCI_DEVICE_ID0x1713 //根据具体设备提供对应的厂商标识及设备标识#define YOUR_PCI_VENDOR_ID 0x13feint main(void){unsigned busnum,devfuncnum; //总线号(PC仅有一条)及设备功能号long address;long io_base; //I/O基地址unsigned char irq; //中断号int pci_index=0 //标识为零标识第一块此种型号设备if(_CA_PCI_Find_Device(YOUR_PCI_DEVICE_ID,YOUR_PCI_VENDOR_ID,pci_index,&busnum,&devfuncnum)!=PCI_SUCCESS){printf(“Can not find device”);exit(EXIT_FAILURE);}//侦测设备中断if(_CA_PCI_Read_Config_Byte(busnum,devfuncnum,offsetof(struct_pci_config_regs,Interrupt_Line),1,&irq)!=PCI_SUCCESS){printf(“Error reading interrupt”);exit(EXIT_FAILURE);}//侦测设备I/O基地址if_CA_PCI_Read_Config_DWord(busnum,devfuncnum,offsetof(struct_pci_config_r

egs,Base_[2]),1,(char *)&address)!=PCI_SUCCESS){printf(“Error reading address”);exit(EXIT_FAILURE);}io_base=PCI_IO_ADDR(adress);printf(“IO address:%x”,io_base);printf(“IRQ:”%x“,irq);exit(EXIT_SUCCESS);}注意:各种设备的Base_Address_Regs[x],x可能不尽相同,需要查看具体的硬件手册决定。2 进入硬件一旦获得了系统分配给某个硬件设备的资源信息,就可以同这个设备进行通信了。至于如何做取决于需要访问的硬件资源。2.1 I/O资源一个进程试图进行I/O操作,必须具有正确的权限等级。你必须是超及用户(root),在编译的时候加上适当参数T1,以确何该进程拥有访问I/O口的.权限。若忽视这一点,该运行进程将获得一个口的权限。若忽视这一点,该运行进行将获得一个SIGSEGV信号,表示一个非法的内存引用,并结束进程运行。现在就可以利用inp、inpd()、inpw(),outp(),inpd(),inpw(0等函数,对I/O基地址(I/O base address)加上寄存器偏移量(offset)处的I/O进行操作了。例如:outpw(baseaddress+offset_reg,0xdeadbeef);此外,对于一些设备,其I/O口是固定、众所皆知的,例如,一块VGA兼容的设备,并无上述所谓基地址。通过0x3c0、0x3d4、0x3d5,可以直接进入这些VGA的控制器。例如:outp(0x3d4,0x11);outp(0x3d5,inp(0x3d5)& ~0x80);2.2 存储映射资源某些设备,可以通过一般的内存操作进入寄存器,这就需要获得内存基地址(memory base address)。为了能够获进入此类设备的寄存器,需要将其映射到驱动程序虚拟地址空间。QNX下的技术资料/etc/readme/technotes/shmem.txt描述了如何创建一个共享内存对象,然后将这个内存对象的一段内存映射到PCI卡中,以便能够进入这个PCI设备。(接着上面的代码)可以利用mmap:char *mem_base;if(PCI_IS_MEM(address)){ //判断内存基地址int fd;char *page_ptr;fd=shm_open(”Physical“,O_RDWR,0777);//创建一个共享内存对象if(fd= =-1){perror(”Error shm_open:“);exit(EXIT_FAILURE);}page_ptr=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,PCI_MEM_ADDR(address)&~0xfff);//将内存基地址映射if(page_ptr= =(char *)perror(”Error mmap:“);exit(EXIT_FAILURE);}mem_base=page_ptr+(PCI_MEM_ADDR(address)&0xfff);close(fd);}printf(”MEM“ address:%lx”,PCI_MEM_ADDR(address));if(PCI_IS_MEM(address))printf(“mapped at : %lx”,mem_base);现在可以使用指针mem_base来进入设备寄存器了。例如:mem_base[SHUTDOWN_REGISTER]=0x0xdeadbeef;2.3 中断资源超级用户(root)可以调用qnx_hint_attach()将一个中断处理程序绑定到一个设备上。中断处理程序作为一个远程调用(far),在进程空间(Localdescriptor Table set)运行。该函数最后一个参数设置数据段。寄存器SS为一个特别的内核栈,这不同于数据段(DS)。因此,需要在中断处理程序及其调用的函数中关断栈检查。大部分系统库中的函数在编译的时候都关断了栈检查,然而,对于需要使用大量内存的函数可能并非如此。后者即是那些在中断处理程序中不可调用的函数,如printf()、open()。通过QNX具体函数在线资源的Safety→Interrupt handler项进行判断该函数是否可以调用。如果函数中包括任何自动(auto)变量,强烈建议将中断函数放在自身文件中,然后利用参数-zu选项编译之。这样能够告知编译器,使得SS!=DS。任何被中断处理程序修改的变量需要指定为volatile关键字。中断处理程序的返回值必须为0;或某个有效的代码号(proxy pid),以此来触发一个代码从而发送一则消息。下面总结一个中断处理程序编写时的注意点:①只能和自己的硬件对话(如,清除设备的中断状态位),千万不要对8259中断控制器编程!②使中断处理程序尽可能的短小。如果有很多的工作需要做,必须触发一个代理,并且它唤醒一个进程完成这些工作,以保证其它进程及低优先级的中断正常运行,提高系统的实时响应能力。③中断处理程序不能调用含有内核调用的例程。④中断处理程序必须是一个远程(far)调用函

数。⑤中断处理程序必须在自己的模块中。⑥无论程序中其它模块是如何编译的,包含中断处理程序的模块必须是利用-zu和-s选项编译。(利用cc-zu-Wc-s)这些选项能够保证SS!=DS,并且关断栈检查。当然,也可使用:#pragma off(check_stack);pid_t far handler_xxx(){return(proxy_xxx);}#pragma on(check_stack);在试图编写执行一个中断处理程序前,务必仔细阅读在线文档。现在,可以参照硬件手册自由地对您的设备寄存器进行操作了。结语在HT-7U极向场电源控制系统中,我们在QNX4.25下开发了多种设备的驱动程序。这些程序工作稳定、性能优异、工作量小且易于控制。此外,QSSL公司的新版本QNX6.x下开发驱动更为方便,其原理同QNX4.25相似或者是对应的。

篇6:Linux下PCI设备驱动程序开发

PCI是一种广泛采用的总线标准,它提供了许多优于其它总线标准(如EISA)的新特性,目前已经成为 计算机系统中应用最为广泛,并且最为通用的总线标准,Linux的内核能较好地支持PCI总线,本文以 Intel 386体系结构为主,探讨了在Linux下开发PCI设备驱动程序的基本框架。

一、PCI总线系统体系结构

PCI是外围设备互连(Peripheral Component Interconnect)的简称,作为一种通用的总线接口标准 ,它在目前的计算机系统中得到了非常广泛的应用。PCI提供了一组完整的总线接口规范,其目的是描述 如何将计算机系统中的外围设备以一种结构化和可控化的方式连接在一起,同时它还刻画了外围设备在连 接时的电气特性和行为规约,并且详细定义了计算机系统中的各个不同部件之间应该如何正确地进行交互 。

无论是在基于Intel芯片的PC机中,或是在基于Alpha芯片的工作站上,PCI毫无疑问都是目前使用最广 泛的一种总线接口标准。同旧式的ISA总线不同,PCI将计算机系统中的总线子系统与存储子系统完全地分 开,CPU通过一块称为PCI桥(PCI- Bridge)的设备来完成同总线子系统的交互,如图1所示。

图1 PCI子系统的体系结构

由于使用了更高的时钟频率,因此PCI总线能够获得比ISA总线更好的整体性能。PCI总线的时钟频率一 般在25MHz到33MHz范围内,有些甚至能够达到66MHz或者133MHz,而在64位系统中则最高能达到266MHz。 尽管目前PCI设备大多采用32位数据总线,但PCI规范中已经给出了64位的扩展实现,从而使PCI总线能够 更好地实现平台无关性,现在PCI总线已经能够用于IA-32、Alpha、PowerPC、SPARC64和IA- 64等体系结 构中。

PCI总线具有三个非常显著的优点,使得它能够完成最终取代ISA总线这一历史使命:

在计算机和外设间传输数据时具有更好的性能;

能够尽量独立于具体的平台;

可以很方便地实现即插即用。

图2 是一个典型的基于PCI总线的计算机系统逻辑示意图,系统的各个部分通过PCI总线和PCI-PCI桥连 接在一起。从图中不难看出,CPU和RAM需要通过PCI桥连接到PCI总线0(即主PCI总线),而具有PCI接口 的显卡则可以直接连接到主PCI总线上。PCI-PCI桥是一个特殊的PCI设备,它负责将PCI总线0和PCI总线1 (即从PCI主线)连接在一起,通常PCI总线1称为PCI-PCI桥的下游(downstream),而PCI总线0 则称为 PCI-PCI桥的上游(upstream)。图中连接到从PCI总线上的是SCSI卡和以太网卡。为了兼容旧的ISA总线 标准,PCI总线还可以通过PCI-ISA桥来连接ISA总线,从而能够支持以前的ISA设备。图中ISA总线上连接 着一个多功能I/O控制器,用于控制键盘、鼠标和软驱。

图2 PCI系统示意图

在此我只对PCI总线系统体系结构作了概括性介绍,如果读者想进一步了解,David A Rusling在The Linux Kernel(http://tldp.org/LDP/tlk/dd/pci.html)中对Linux的PCI子系统有比较详细的介绍。

二、Linux驱动程序框架

Linux将所有外部设备看成是一类特殊文件,称之为“设备文件”,如果说系统调用是Linux内核和应 用程序之间的接口,那么设备驱动程序则可以看成是 Linux内核与外部设备之间的接口。设备驱动程序向 应用程序屏蔽了硬件在实现上的细节,使得应用程序可以像操作普通文件一样来操作外部设备。

1. 字符设备和块设备

Linux 抽象了对硬件的处理,所有的硬件设备都可以像普通文件一样来看待:它们可以使用和操作文 件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是 要实现这些系统调用函数。Linux系统中的所有硬件设备都使用一个特殊的设备文件来表示,例如,系统 中的第一个IDE 硬盘使用/dev/hda表示。每个设备文件对应有两个设备号:一个是主设备号,标识该设备 的种类,也标识了该设备所使用的驱动程序;另一个是次设备号,标识使用同一设备驱动程序的不同硬件 设备。设备文件的主设备号必须与设备驱动程序在登录该设备时申请的主设备号一致,否则用户进程将无 法访问到设备驱动程序。

在Linux操作系统下有两类主要的设备文件:一类是字符设备,另一类则是块设备。字符设备是以字节 为单位逐个进行 I/O操作的设备,在对字符设备发出读写请求时,实际的硬件I/O紧接着就发生了,一般 来说字符设备中的缓存是可有可无的,而且也不支持随机访问。块设备则是利用一块系统内存作为缓冲区 ,当用户进程对设备进行读写请求时,驱动程序先查看缓冲区中的内容,如果缓冲区中的数据能满足用户 的要求就返回相应的数据,否则就调用相应的请求函数来进行实际的I/O操作。块设备主要是针对磁盘等 慢速设备设计的,其目的是避免耗费过多的CPU时间来等待操作的完成。一般说来,PCI卡通常都属于字符 设备。

所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到。使 用mknod命令可以创建指定类型的设备文件,同时为其分配相应的主设备号和次设备号。例如,下面的命 令:

[root@gary root]# mknod /dev/lp0 c 6 0

将建立一个主设备号为6,次设备号为0的字符设备文件/dev/lp0。当应用程序对某个设备文件进行系 统调用时,Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到 核心态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。

2. 设备驱动程序接口

Linux中的I/O子系统向内核中的其他部分提供了一个统一的标准设备接口,这是通过 include/linux/fs.h中的数据结构file_operations来完成的:

struct file_operations { struct module *owner; 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 file *, struct dentry *, int datasync); 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 *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);};

当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过 file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调 用file_operations结构中的read函数。

2. 设备驱动程序模块

Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则 是编译成可以动态加载的模块。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且 不能动态地卸载,不利于调试,所有推荐使用模块方式。

从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者 C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。

在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module( )和cleanup_module( ),而 且至少要包含和两个头文件。在用gcc编译内核模块时 ,需要加上-DMODULE -D__KERNEL__ -DLINUX这几个参数,编译生成的模块(一般为.o文件)可以使用命 令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用模块中的函数init_module( ) 。当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module( )。 任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。

3. 设备驱动程序结构

了解设备驱动程序的基本结构(或者称为框架),对开发人员而言是非常重要的,Linux的设备驱动程 序大致可以分为如下几个部分:驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控 制操作、设备的中断和轮询处理。

驱动程序的注册与注销

向系统增加一个驱动程序意味着要赋予它一个主设备号,这可以通过在驱动程序的初始化过程中调用 register_chrdev( )或者register_blkdev( )来完成。而在关闭字符设备或者块设备时,则需要通过调用 unregister_chrdev( )或unregister_blkdev( )从内核中注销设备,同时释放占用的主设备号。

设备的打开与释放

打开设备是通过调用file_operations结构中的函数open( )来完成的,它是驱动程序用来为今后的操 作完成初始化准备工作的。在大部分驱动程序中,open( )通常需要完成下列工作:

检查设备相关错误,如设备尚未准备好等。

如果是第一次打开,则初始化硬件设备。

识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。

分配和填写要放在file->private_data里的数据结构。

使用计数增1。

释放设备是通过调用file_operations结构中的函数release( )来完成的,这个设备方法有时也被称为 close( ),它的作用正好与open( )相反,通常要完成下列工作:

使用计数减1。

释放在file->private_data中分配的内存。

如果使用计算为0,则关闭设备。

设备的读写操作

字符设备的读写操作相对比较简单,直接使用函数read( )和write( )就可以了。但如果是块设备的话 ,则需要调用函数block_read( )和block_write( )来进行数据读写,这两个函数将向设备请求表中增加 读写请求,以便Linux内核可以对请求顺序进行优化。由于是对内存缓冲区而不是直接对设备进行操作的 ,因此能很大程度上加快读写速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据 写入设备,那么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数request_fn( )来完成的。

设备的控制操作

除了读写操作外,应用程序有时还需要对设备进行控制,这可以通过设备驱动程序中的函数ioctl( ) 来完成。ioctl( )的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。

设备的中断和轮询处理

对于不支持中断的硬件设备,读写时需要轮流查询设备状态,以便决定是否继续进行数据传输。如果 设备支持中断,则可以按中断方式进行操作。

三、PCI驱动程序实现

1. 关键数据结构

PCI 设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备 上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的 PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号 以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该 变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备 号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标 来表示次设备号。

在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:

pci_driver

这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程 序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设 备的函数remove( ):

struct pci_driver { struct list_head node; char *name; const struct pci_device_id *id_table; int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); void (*remove) (struct pci_dev *dev); int (*save_state) (struct pci_dev *dev, u32 state); int (*suspend)(struct pci_dev *dev, u32 state); int (*resume) (struct pci_dev *dev); int (*enable_wake) (struct pci_dev *dev, u32 state, int enable);};

pci_dev

这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息 ,包括厂商ID、设备ID、各种资源等:

struct pci_dev { struct list_head global_list; struct list_head bus_list; struct pci_bus *bus; struct pci_bus *subordinate; void *sysdata; struct proc_dir_entry *procent; unsigned int devfn; unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; u8 hdr_type; u8 rom_base_reg; struct pci_driver *driver; void *driver_data; u64 dma_mask; u32 current_state; unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE]; unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE]; unsigned int irq; struct resource resource[DEVICE_COUNT_RESOURCE]; struct resource dma_resource[DEVICE_COUNT_DMA]; struct resource irq_resource[DEVICE_COUNT_IRQ]; char name[80]; char slot_name[8]; int active; int ro; unsigned short regs; int (*prepare)(struct pci_dev *dev); int (*activate)(struct pci_dev *dev); int (*deactivate)(struct pci_dev *dev);};

2. 基本框架

在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打 开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块,

下面给出一个典型的PCI 设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

/* 指明该驱动程序适用于哪一些PCI设备 */static struct pci_device_id demo_pci_tbl [] __initdata = { {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO, PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO}, {0,}};/* 对特定PCI设备进行描述的数据结构 */struct demo_card { unsigned int magic; /* 使用链表保存所有同类的PCI设备 */ struct demo_card *next; /* ... */}/* 中断处理模块 */static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs){ /* ... */}/* 设备文件操作接口 */static struct file_operations demo_fops = { owner: THIS_MODULE, /* demo_fops所属的设备模块 */ read: demo_read, /* 读设备操作*/ write: demo_write, /* 写设备操作*/ ioctl: demo_ioctl, /* 控制设备操作*/ mmap: demo_mmap, /* 内存重映射操作*/ open: demo_open, /* 打开设备操作*/ release: demo_release /* 释放设备操作*/ /* ... */};/* 设备模块信息 */static struct pci_driver demo_pci_driver = { name: demo_MODULE_NAME, /* 设备模块名称 */ id_table: demo_pci_tbl, /* 能够驱动的设备列表 */ probe: demo_probe, /* 查找并初始化设备 */ remove: demo_remove /* 卸载设备模块 */ /* ... */};static int __init demo_init_module (void){ /* ... */}static void __exit demo_cleanup_module (void){ pci_unregister_driver(&demo_pci_driver);}/* 加载驱动程序模块入口 */module_init(demo_init_module);/* 卸载驱动程序模块入口 */module_exit(demo_cleanup_module);

上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是 ,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、 __exit等标志符,以使同普通函数 区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

3. 初始化设备模块

在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

检查PCI总线是否被Linux内核支持;

检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。

读出配置头中的信息提供给驱动程序使用。

当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立 起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代 码:

static int __init demo_init_module (void){ /* 检查系统是否支持PCI总线 */ if (!pci_present()) return -ENODEV; /* 注册硬件驱动程序 */ if (!pci_register_driver(&demo_pci_driver)) { pci_unregister_driver(&demo_pci_driver); return -ENODEV; } /* ... */ return 0;}

驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线 结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就 必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备, 但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一 个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。

static int __init demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id){ struct demo_card *card; /* 启动PCI设备 */ if (pci_enable_device(pci_dev)) return -EIO; /* 设备DMA标识 */ if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) { return -ENODEV; } /* 在内核空间中动态申请内存 */ if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) { printk(KERN_ERR “pci_demo: out of memoryn”); return -ENOMEM; } memset(card, 0, sizeof(*card)); /* 读取PCI配置信息 */ card->iobase = pci_resource_start (pci_dev, 1); card->pci_dev = pci_dev; card->pci_id = pci_id->device; card->irq = pci_dev->irq; card->next = devs; card->magic = DEMO_CARD_MAGIC; /* 设置成总线主DMA模式 */ pci_set_master(pci_dev); /* 申请I/O资源 */ request_region(card->iobase, 64, card_names[pci_id->driver_data]); return 0;}

4. 打开设备模块

在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候, 非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。

static int demo_open(struct inode *inode, struct file *file){ /* 申请中断,注册中断处理程序 */ request_irq(card->irq, &demo_interrupt, SA_SHIRQ, card_names[pci_id->driver_data], card)) { /* 检查读写模式 */ if(file->f_mode & FMODE_READ) { /* ... */ } if(file->f_mode & FMODE_WRITE) { /* ... */ } /* 申请对设备的控制权 */ down(&card->open_sem); while(card->open_mode & file->f_mode) { if (file->f_flags & O_NONBLOCK) {/* NONBLOCK模式,返回-EBUSY */up(&card->open_sem);return -EBUSY; } else {/* 等待调度,获得控制权 */card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);up(&card->open_sem);/* 设备打开计数增1 */MOD_INC_USE_COUNT;/* ... */ } }}

5. 数据读写和控制信息模块

PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制 的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

static int demo_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg){ /* ... */ switch(cmd) { case DEMO_RDATA:/* 从I/O端口读取4字节的数据 */val = inl(card->iobae + 0x10);/* 将读取的数据传输到用户空间 */return 0; } /* ... */}

事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的 driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上, 除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射 后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过 DMA传送到系统内存中。

6. 中断处理模块

PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。 当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs){ struct demo_card *card = (struct demo_card *)dev_id; u32 status; spin_lock(&card->lock); /* 识别中断 */ status = inl(card->iobase + GLOB_STA); if(!(status & INT_MASK)) { spin_unlock(&card->lock); return; /* not for us */ } /* 告诉设备已经收到中断 */ outl(status & INT_MASK, card->iobase + GLOB_STA); spin_unlock(&card->lock); /* 其它进一步的处理,如更新DMA缓冲区指针等 */}

7. 释放设备模块

释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备 模块相反:

static int demo_release(struct inode *inode, struct file *file){ /* ... */ /* 释放对设备的控制权 */ card->open_mode &= (FMODE_READ | FMODE_WRITE); /* 唤醒其它等待获取控制权的进程 */ wake_up(&card->open_wait); up(&card->open_sem); /* 释放中断 */ free_irq(card->irq, card); /* 设备打开计数增1 */ MOD_DEC_USE_COUNT; /* ... */ }

8. 卸载设备模块

卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数 pci_unregister_driver( )从Linux内核中注销设备驱动程序:

static void __exit demo_cleanup_module (void){ pci_unregister_driver(&demo_pci_driver);}

四、小结

PCI总线不仅是目前应用广泛的计算机总线标准,而且是一种兼容性最强、功能最全的计算机总线。而 Linux作为一种新的操作系统,其发展前景是无法估量的,同时也为PCI总线与各种新型设备互连成为可能 。由于Linux源码开放,因此给连接到PCI总线上的任何设备编写驱动程序变得相对容易。本文介绍如何编 译Linux下的PCI驱动程序,针对的内核版本是2.4。

篇7:Windows设备驱动程序的研制开发

Windows设备驱动程序的研制开发

引言:

由于工作关系,我经常涉及PC机与外围设备接口的工作,从PC机这方面要做的工作看来,主要是通过接口处理外围设备的中断,通过I/O端口或内存地址与外设互相传递数据。从计算机原理的角度看,所要达到的目的很简单,那么如何编写程序完成上述功能呢?

目前国内流行的PC操作系统有三种:DOS,Win95/98系列,WindowsNT。DOS是单用户、单任务操作系统,由于PC机硬件处理速度不断提高,基于单用户、单任务的操作系统越来越不能充分发挥硬件的功能,现在只应用于一些老式PC及其它个别场合,有逐渐被淘汰的趋势;Win95/98系列和WindowsNT属于多任务操作系统,不论从其原理还是界面上看,这两种操作系统都比DOS有着无可比拟的优越性,这两种操作系统虽然在界面和操作上及其相似,但其内部实现的诸多方面有许多区别,有些区别是本质上的。Win95/98设计目标是针对一般家庭用户,安全性及可靠性存在许多薄弱环节,就可靠性而言,Win95/98系列不能很好的防止多任务环境中某个进程的非法操作导致系统中其它程序甚至整个系统的'崩溃,而WindowsNT在这方面及其它诸多方面设计的相当严谨。这两种操作系统是Microsoft公司同一时期的产品,但针对不同的使用群,所以在一些重要场合及生产实践中应该选择WindowsNT作为计算机的操作系统,此外,从发展趋势来看,WindowsNT已经成为定型产品,具有相对稳定性。

在不同操作系统下编写驱动程序是有很大区别的,在DOS平台上,应用程序和设备驱动程序之间没有标准的接口,它们在外部表现为一个扩展名为EXE的文件,驱动程序的作用被柔和在应用程序中,这样,应用程序为了使用不同厂商的同一类设备,必须了解这些设备在接口上具体的硬件实现,同时,对于一个特定型号的硬件产品,所有支持它的应用软件中对于控制整个设备动作的这部分代码,可能被多次重写。这种情况不适应硬件及应用软件的飞速发展。Windows系统在这方面,进行了根本性改进,把控制设备动作的这部分代码独立出来,提出了设备驱动程序的概念,驱动程序是应用程序和硬件设备之间的一个桥梁,应用程序与驱动程序之间有明确的接口,应用程序通过与驱动程序交换信息,达到控制外设的目的。接口定义的操作是面向设备的,这就是说,在应用程序的设计中,并不用关心对外设操作的具体硬件实现,只是对驱动程序发出一系列指令既可;驱动程序接受来自上层应用程序的指示,具体操纵实际硬件,完成用户功能。具体实现上,Win95/98系列与WindowsNT又有所区别,WindowsNT是严格按照上述思路设计的;而Win95/98系列不那么严格,其支持上述思路,但同时应用程序也可以绕过驱动程序直接访问实际物理I/O,这样做,增加程序设计的灵活性,但同时,对系统可靠性造成一定隐患。这也正是Win95/98系列可靠性低于WinNT的原因之一。

表1-1 三种操作系统下访问接口比较

操作系统应用程序访问接口方式访问权限DOS直接访问所有[注]Windows95/98通过设备驱动程序*.VXD所有[注]直接访问仅I/O端口WindowsNT

[1] [2] [3] [4] [5]

篇8:编写Linux操作系统设备驱动程序概述

1.1 Linux设备驱动程序分类

Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加, 主要是驱动程序的增加,在Linux内核的不断升级过程中,驱动程序的结构还是相对稳定。在2.0.xx到2.2.xx的变动里,驱动程序的编写做了一些改变,但是从2.0.xx的驱动到 2.2.xx的移植只需做少量的工作。Linux系统的设备分为字符设备(char device),块设备(block device)和网络设备(net work device)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支 持,并且块设备必须能够随机存取(random access),字符设备则没有这个要求。典型的 字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。 网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制 。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

1.2 编写驱动程序的一些基本概念

无论是什么操作系统的驱动程序,都有一些通用的概念。操作系统提供给驱动程序的支持也大致同。下面简单介绍一下网络设备驱动程序的一些基本要求。

1.2.1 发送和接收

这是一个网络设备最基本的功能。一块网卡所做的无非就是收发工作。所以驱动程序里要告诉系统发送函数在哪里,系统在有数据要发送时就会调用你的发 送程序。还有 驱动程序由于是直接操纵硬件的,所以网络硬件有数据收到最先能得到这个数据的也就是驱动程序,它负责把这些原始数据进行必要的处理然后送给系统。这里,操作系统必须要提供两个机制,一个是找到驱动程序的发送函数,一个是驱动程序把收到的数据送给系统。

1.2.2 中断

中断在现代计算机结构中有重要的地位。操作系统必须提供驱动程序响应中断的能力。 一般是把一个中断处理程序注册到系统中去。操作系统在硬件中断发生后 调用驱动程序 的处理程序。Linux支持中断的共享,即多个设备共享一个中断。

1.2.3 时钟

在实现驱动程序时,很多地方会用到时钟。如某些协议里的超时处理,没有中断机制的 硬件的轮询等。操作系统应为驱动程序提供定时机制。一般是在预定的时 间过了以后回调注册的时钟函数。在网络驱动程序中,如果硬件没有中断功能,定时器可以提供轮询(poll)方式对硬件进行存取。或者是实现某些协议时需要的超时重传等。

二.Linux系统网络设备驱动程序

2.1 网络驱动程序的结构

所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象的方法。一个设备就是一个对象(device 结构),它内部有自己的数据和方法。每一个设备的方法被调用时的第一个参数都是这个设备对象本身。这样这个方法就可以存取自身的数据(类似面向对象程序设计时的this引用)。

一个网络设备最基本的方法有初始化、发送和接收。

------------------- ---------------------

|deliver packets | |receive packets queue|

|(dev_queue_xmit()) | |them(netif_rx()) |

------------------- ---------------------

| | /

/ | |

-------------------------------------------------------

| methods and variables(initialize,open,close,hard_xmit,|

| interrupt handler,config,resources,status...) |

-------------------------------------------------------

| | /

/ | |

----------------- ----------------------

|send to hardware | |receivce from hardware|

----------------- ----------------------

| | /

/ | |

-----------------------------------------------------

| hardware media |

-----------------------------------------------------

初始化程序完成硬件的初始化、device中变量的初始化和系统资源的申请。发送程序是在驱动程序的上层协议层有数据要发送时自动调用的。一般驱动程序中不对发送数据进行缓存,而是直接使用硬件的发送功能把数据发送出去。接收数据一般是通过硬件中断来通知的。在中断处理程序里,把硬件帧信息填入一个skbuff结构中,然后调用netif_ rx()传递给上层处理。

2.2 网络驱动程序的基本方法

网络设备做为一个对象,提供一些方法供系统访问。正是这些有统一接口的方法,掩蔽 了硬件的具体细节,让系统对各种网络设备的访问都采用统一的形式,做到硬件无关性 。

下面解释最基本的方法。

2.2.1 初始化(initialize)

驱动程序必须有一个初始化方法。在把驱动程序载入系统的时候会调用这个初始化程序 。它做以下几方面的工作。检测设备。在初始化程序里你可以根据硬件的特征检查硬件是否存在,然后决定是否启动这个驱动程序。配置和初始化硬件。在初始化程序里你可以完成对硬件资源的配置,比如即插即用的硬件就可以在这个时候进行配置(Linux内核对PnP功能没有很好的支持,可以在驱动程序里完成这个功能)。配置或协商好硬件占用的资源以后,就可以向系统申请这些资源。有些资源是可以和别的设备共享的,如中断。有些是不能共享的,如IO、DMA。接下来你要初始化device结构中的变量。最后,你可以让硬件正式开始工作。

2.2.2 打开(open)

open这个方法在网络设备驱动程序里是网络设备被激活的时候被调用(即设备状态由down-->up),

所以实际上很多在initialize中的工作可以放到这里来做。比如资源的申请,硬件的激活。如果dev->open返回非0(error),则硬件的状态还是down。 open方法另一个作用是如果驱动程序做为一个模块被装入,则要防止模块卸载时设备处 于打开状态。在open方法里要调用MOD_INC_USE_COUNT宏。

2.2.3 关闭(stop)

close方法做和open相反的工作。可以释放某些资源以减少系统负担。close是在设备状态由up转为down时被调用的。另外如果是做为模块装入的驱动程序,close里应该调用MOD_DEC_USE_COUNT,减少设备被引用的次数,以使驱动程序可以被卸载。另外close方法必须返回成功(0==success)。

2.2.4 发送(hard_start_xmit)

所有的网络设备驱动程序都必须有这个发送方法。在系统调用驱动程序的xmit时,发送 的数据放在一个sk_buff结构中。一般的驱动程序把数据传给硬件发出去。也有一些特殊 的设备比如loopback把数据组成一个接收数据再回送给系统,或者dummy设备直接丢弃数据。如果发送成功,hard_start_xmit方法里释放sk_buff,返回0(发送成功)。如果设备暂时 无法处理,比如硬件忙,则返回1。这时如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0以后才会再次发送。tbusy的置0任务一般由中断完成。硬件在发送 结束后产生中断,这时可以把tbusy置0,然后用mark_bh()调用通知系统可以再次发送。 在发送不成功的情况下,也可以不置dev->tbusy为非0,这样系统会不断尝试重发。如果hard_start_xmit发送不成功,则不要释放sk_buff。传送下来的sk_buff中的数据已经包含硬件需要的帧头。所以在发送方法里不需要再填充硬件帧头,数据可以直接提交给硬 件发送。sk_buff是被锁住的(locked),确保其他程序不会存取它。

2.2.5 接收(reception)

驱动程序并不存在一个接收方法。有数据收到应该是驱动程序来通知系统的。一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff(skb),从 硬件读出数据放置到申请好的缓冲区里。接下来填充sk_buff中 的一些信息。skb->dev= dev,判断收到帧的协议类型,填入skb->protocol(多协 议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置skb->pkt_type,标明第二 层(链路层)数据类型。可以是以下类型:

PACKET_BROADCAST : 链路层广播;

PACKET_MULTICAST : 链路层组播;

PACKET_SELF : 发给自己的帧;

PACKET_OTHERHOST : 发给别人的帧(监听模式时会有这种帧)。

最后调用netif_rx()把数据传送给协议层。netif_rx()里数据放入处理队列然后返回, 真正的处理是在中断返回以后,这样可以减少中断时间。调用netif_rx()以后, 驱动程序就不能再存取数据缓冲区skb。

2.2.6 硬件帧头(hard_header)

硬件一般都会在上层数据发送之前加上自己的硬件帧头,比如以太网(Ethernet)就有14字节的帧头。这个帧头是加在上层ip、ipx等数据包的前面的。驱动程序提供一个hard_ header方法,协议层(ip、ipx、arp等)在发送数据之前会调用这段程序。 硬件帧头的长度必须填在dev->hard_header_len,这样协议层回在数据之前保留好硬件 帧头的空间。这样hard_header程序只要调用skb_push然后正确填入硬件帧头就可以了。

在协议层调用hard_header时,传送的参数包括(2.0.xx):数据的sk_buff,device指针 ,protocol,目的地址(daddr),源地址(saddr),数据长度(len)。数据长度不要使用s k_buff中的参数,因为调用hard_header时数据可能还没完全组织好。saddr是NULL的话是使用缺省地址(default)。daddr是NULL表明协议层不知道硬件目的地址。如果hard_header完全填好了硬件帧头,则返回添加的字节数。如果硬件帧头中的信息还不完全(比如daddr为NULL,但是帧头中需要目的硬件地址。典型的情况是以太网需要地址解析(arp)),则返回负字节数。hard_header返回负数的情况下,协议层会做进一步的build header的工作。目前Linux系统里就是做arp (如果hard_header返回正,dev->arp=1,表明不需要做arp,返回负,dev->arp=0,做arp)。 对hard_header的调用在每个协议层的处理程序里。如ip_output。

2.2.7 地址解析(xarp)

有些网络有硬件地址(比如Ethernet),并且在发送硬件帧时需要知道目的硬件地址。这样就需要上层协议地址(ip、ipx)和硬件地址的对应。这个对应是通过地址解析完成的。需要做arp的的设备在发送之前会调用驱动程序的rebuild_header方法。调用的主要参数包括指向硬件帧头的指针,协议层地址。如果驱动程序能够解析硬件地址,就返回1,如果不能,返回0。对rebuild_header的调用在net/core/dev.c的do_dev_queue_xmit()里。

2.2.8 参数设置和统计数据

在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有: dev->set_mac_address()。

当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。 dev->set_config() 。当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法。用户会传递一个ifmap结构包含需要的I/O、中断等参数。dev->do_ioctl()

如果用户调用ioctl时类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统会调用驱动程序的这个方法。一般是设置设备的专用数据。读取信息也是通过ioctl调用进行。除次之外驱动程序还可以提供一个dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。ioc

tl的处理在net/core/dev.c的dev_ioctl()和dev_ifsioc()里。

浅析外商在华办事处的低成本运行与管理

浅析证券报道的法律规范

浅析环境税的立法策略和立法原则毕业论文

浅析现行中小企业成本核算管理的论文

浅析进程“伪隐藏”技术与实现两则

浅析建筑施工企业合同索赔与反索赔

水利工程堤防建设与防洪建设浅析论文

浅析信息网的建设对于工程管理的重要作用论文

火电质量控制工程施工论文

浅析智能光网络技术及发展

浅析设备驱动程序通知应用程序的几种方法
《浅析设备驱动程序通知应用程序的几种方法.doc》
将本文的Word文档下载到电脑,方便收藏和打印
推荐度:
点击下载文档

【浅析设备驱动程序通知应用程序的几种方法(精选8篇)】相关文章:

浅析中职物理教学综合实习论文2022-09-19

电子信息工程毕业论文参考文献2022-12-08

市政道路软基施工的论文2022-12-22

企业网络安全的几种常见攻击手段2023-06-07

浅析探望权的法律问题 论文2022-08-14

电子商务毕业论文选题2023-10-29

高中化学论文:当前实施探究式教学中存在的问题与解决策略2023-06-07

污水处理系统架构设计分析论文2023-01-15

谈会计电算化信息系统的企业内部控制毕业论文2022-12-26

提升项目可行性研究报告编制水准应注意的问题2023-01-19

点击下载本文文档