精华内容
下载资源
问答
  • 本文主要是一个简单字符驱动程序的实现,主要涉及三个部分,即外围驱动、Makefile以及测试程序的编写;在《LDD3》一书中有提到,用户空间的驱动程序有以下优缺点: 优势: ①可以和整个C库链接;②可使用普通的...

    一、前言

    驱动一般分为字符设备驱动、块设备驱动与网络驱动三种类型。本文主要是一个简单字符驱动程序的实现,主要涉及三个部分,即外围驱动、Makefile以及测试程序的编写;在《LDD3》一书中有提到,用户空间的驱动程序有以下优缺点:

    优势:

    ①可以和整个C库链接;②可使用普通的调试器调试驱动程序代码,不用调试正在运行的内核;③程序的崩溃不会影响系统的正常运行,简单地kill掉就OK;④和内核的内存不同,用户内存可以换出;良好设计的驱动程序仍然支持对设备的并发访问;⑤如果须编写闭源码的驱动程序,则用户空间驱动程序可更加容易地避免因修改内核接口而导致的不明确的许多问题。

    缺陷:

    ①中断在用户空间不可用;②只能通过mmap映射才能直接访问内存;③只能调用ioperm或iopl后才能访问I/O端口;④客户端与硬件之间传递数据和动作需要上下文切换,即响应时间很慢;⑤ 如果驱动被换出到磁盘,响应速度会更慢;⑥用户空间不能处理像网络接口、块设备等重要的设备;

    二、外围驱动模块部分

    关于一个数模转换芯片外围驱动、Makefile以及测试程序的编写,通过向芯片写入数据来控制模拟电压的输出,由于开始使用的2片max5141/max5142(14 bit_data)芯片,每pcs只有一路输出,而且还要用到i2c总线,所以用一片2路输出的TLV5648AID(12 bit_data)替换2片max5141/max5142更方便,节约硬件资源。它们PIN如图:
    这里写图片描述这里写图片描述

    驱动代码主要由4个函数构成,它们分别是:
    ①ssize_t tlv5618aid_write (struct file *filp, const short __user *buf_user, size_t size, loff_t *f_pos);
    ②static int tlv5618aid_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg);
    ③static int __init (void);
    ④static void __exit s3c6410_tlv5618aid_exit(void);
    其中tlv5618aid_ioctl函数提供模式的选取。

    1、tlv5618aid_write函数主要功能

    5618芯片DIN传输的格式:
    这里写图片描述
    data为写入的数据,参考以上数据的传输格式:获取两种模式下各要处理的数据,代码如下:

    data_fid_wtoa = data | 0xc000;//A端输出高位数据格式
    data_vsk_wtob = data | 0x4000;//B端输出高位数据格式

    ARM的GPIO与5618芯片连接的电路原理图:
    这里写图片描述
    5618数据传输的时序图:
    这里写图片描述
    注意每次操作完数据有个从低电平到高电平的转换,5141的时序就没有此要求,这算个细节问题,处理8bit数据的代码如下:

    for(i = 8; i; i--)//高8位数据处理,遍历一次处理一位数据
        {
            gpio_set_value(S3C64XX_GPQ(6), 1);//时钟信号高电平
            udelay(20);
    
            tmp = data_high & 0x80;//取8位数据中的最高位
            gpio_set_value(S3C64XX_GPQ(4), tmp);//数据在高低电平切换时保持不变,参考芯片datasheet的时序图
            udelay(20);
    
            gpio_set_value(S3C64XX_GPQ(6), 0);//时钟信号低电平
            udelay(20);
            data_high = data_high <<1;//数据左移1位
        }

    2、tlv5618aid_ioctl函数主要功能

    该函数实现对模式的选取,不用编译进内核,不需要考虑幻数、序列、传送方向以及参数的大小,代码如下:

    switch(cmd) {
        case 0: port = 0; //FID
            break;
        case 1: port = 1; //VSK
            break;
        default:
            return -EINVAL;
        }

    3、s3c6410_tlv5618aid_init函数主要功能

    主要实现设备的注册(register_chrdev_region函数)、初始化(cdev_init函数)、添加(cdev_add函数)及创建(class_create及device_create函数),以及GPIO的配置,下面主要讲下GPIO的配置,其他部分后面的源码有注释。6410ARM板的GPIO功能配置说明书如下:
    ①先配置上拉:
    这里写图片描述
    代码如下:

    tmp = __raw_readl(S3C64XX_GPOPUD);//读取原来GPIO的数据
    tmp &= (~0xc00);//先把位[2*5+1,2*5]清0,其他位不变tmp=tmp & 0x0011 1111 1111
    tmp |= 0x800;//再把位[2*5+1,2*5]的置为10,tmp=tmp | 0x1000 0000 0000上拉配置成功,
    __raw_writel(tmp,S3C64XX_GPOPUD)

    ②配置成输出功能:
    这里写图片描述
    代码如下:

    tmp = __raw_readl(S3C64XX_GPOCON);
    tmp &= (~0xc00);//先把位[11,10]清0,其他位不变tmp=tmp & 0x0011 1111 1111
    tmp |= 0x400;//再把位[11,10]的置为01,tmp=tmp | 0x0100 0000 0000输出模式配置成功,
    __raw_writel(tmp,S3C64XX_GPOCON);

    ③数据的传输:
    这里写图片描述
    代码如下:

    tmp = __raw_readl(S3C64XX_GPODAT);
    tmp &= (~0x20);
    tmp |= 0x20;//输出高电平1,0x10 0000,第5位输出为1(从0开始计算)
    __raw_writel(tmp,S3C64XX_GPODAT);

    3、s3c6410_tlv5618aid_exit函数主要功能

    主要实现设备在系统中的删除(cdev_del函数)、释放注册的设备(unregister_chrdev_region函数)以及移除操作(device_destroy及class_destroy函数),你会发现函数的执行顺序与s3c6410_tlv5618aid_init里相对应的函数执行顺利一致。

    4、重要的file_operations数据结构

    该结构体用来存储驱动内核模块提供的对设备各种操作的函数指针,file_operations结构体本该有许多函数,但只需实现需要用到的函数。本文只实现了2个函数:tlv5618aid_ioctl与tlv5618aid_write函数。
    驱动源代码如下:

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/timer.h>
    #include <linux/miscdevice.h>
    #include <linux/delay.h>
    #include <linux/device.h>
    #include <linux/cdev.h>
    #include <asm/irq.h>
    #include <mach/gpio.h>
    #include <plat/regs-gpio.h>
    #include <plat/gpio-cfg.h>
    #include <mach/hardware.h>
    #include <linux/io.h>
    
    #define DEVICE_NAME "tlv5618aid"        //设备名称
    #define TLV5618AID_MAJOR 231        //主设备号
    
    static char port;
    
    //size_t表示无符号的,即typedef unsigned (long/int) size_t(64位机/32位机,具体表示long还是int看是几位机) 
    //而ssize_t 对应有符号的,即typedef signed (long/int)ssize_t(64位机/32位机)
    ssize_t tlv5618aid_write (struct file *filp, const short __user *buf_user, size_t size, loff_t *f_pos)
    //filp为文件指针,buf_user为指向用户空间的缓冲区(传输的数据为16位,用short型,8位用char就够用了),size为请求数据的长度,offp为指向“long offset type”对象的指针,该对象为用户在文件中进行存取操作的位置
    {                                                   
        unsigned long tmp;
        unsigned short data, data_fid_wtoa, data_vsk_wtob;
        unsigned char i;//写数据的遍历次数
        unsigned char data_high, data_low;//数据的高八位与低八位
    
        data = *buf_user;//获取需要写的数据
        //printk("the dataddd of buf_user = %d\n", data);   
        data_fid_wtoa = data | 0xc000;//A端输出高位数据格式
        data_vsk_wtob = data | 0x4000;//B端输出高位数据格式
    
    #ifdef TIMER
        struct timeval start, stop;//计时结构体
        unsigned int    usec;
        do_gettimeofday(&start);//获取以下代码执行的开始时间
        printk("time = %02d.%06d\n", start.tv_sec, start.tv_usec);
    #endif
    
        if(port == 0) // 模式0_ FID
        {
            data_low  = (char)data_fid_wtoa;//short强制转换为char型(16位数据转换为8位),得到低8位数据
            data_high = (char)(data_fid_wtoa >> 8);//右移8位后再强制转换得到高8位数据
        }
        if(port == 1)//模式1_ VSK
        {
            data_low  = (char)data_vsk_wtob;
            data_high = (char)(data_vsk_wtob >> 8);
        }
    
        gpio_set_value(S3C64XX_GPO(5), 0);//片选该芯片,CS=0,低电平有效
        udelay(20); 
    
        for(i = 8; i; i--)//高8位数据处理,遍历一次处理一位数据
        {
            gpio_set_value(S3C64XX_GPQ(6), 1);//时钟信号高电平
            udelay(20);
            tmp = data_high & 0x80;//取8位数据中的最高位
            gpio_set_value(S3C64XX_GPQ(4), tmp);//数据在高低电平切换时保持不变,参考芯片datasheet的时序图
            udelay(20);
            gpio_set_value(S3C64XX_GPQ(6), 0);//时钟信号低电平
            udelay(20);
            data_high = data_high <<1;//数据左移1位
        }
    
        for(i = 8; i; i--)//低8位数据的处理
        {
            gpio_set_value(S3C64XX_GPQ(6), 1);
            udelay(20); 
            tmp = data_low & 0x80;
            gpio_set_value(S3C64XX_GPQ(4), tmp);
            udelay(20);
            gpio_set_value(S3C64XX_GPQ(6), 0);          
            udelay(20);
            data_low = data_low << 1;
        }
        gpio_set_value(S3C64XX_GPQ(6), 1);//结束需要向高电平跳变一次
        gpio_set_value(S3C64XX_GPO(5), 1);      
    
    #ifdef TIMER
            do_gettimeofday(&stop);//获取以上代码运行结束的时间 
            printk("time = %02d.%06d\n", stop.tv_sec, stop.tv_usec);
            if(stop.tv_usec >= start.tv_usec) {
            usec = stop.tv_usec - start.tv_usec;
            } else {
            usec = stop.tv_usec + 1000000 - start.tv_usec;
            }
            printk("time lapse: = %d us\n", usec);
    #endif
        return 0;
    }
    
    static int tlv5618aid_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
    //cmd为用户空间传递给驱动程序的命令,可选arg参数无论用户程序使用的是指针还是整数值,它都以unsigned long的形式传递给驱动程序
    {
        switch(cmd) {
        case 0: port = 0; //FID
            break;
        case 1: port = 1; //VSK
            break;
        default:
            return -EINVAL;
        }
        return 0;
    }
    
    static struct file_operations s3c6410_tlv5618aid_fops = {
        .owner  =   THIS_MODULE,
        .ioctl  =   tlv5618aid_ioctl,
        .write  =   tlv5618aid_write,
    };
    //该结构体用来存储驱动内核模块提供的对设备各种操作的函数指针,file_operations结构体本该有许多函数,但只需实现需要用到的函数。
    
    //cdev表示字符设备的一个结构体,头文件<linux/cdev.h>
    static struct cdev cdev_tlv5618aid;
    struct class * my_class;
    
    static int __init s3c6410_tlv5618aid_init(void)
    {
        int ret;
        unsigned long tmp;
        dev_t devno;//dev_t表示一个32位的数,其中12位为主设备号,剩下20位为次设备号
        printk(KERN_NOTICE "enter tlv5618aid_init\n");
        devno = MKDEV(TLV5618AID_MAJOR,0);      
        //将主设备号为TLV5618AID_MAJOR,次设备号为0转换成dev_t型
    
        ret = register_chrdev_region(devno,1,DEVICE_NAME);
        //目的得到设备编号(注册编号),第一个参数为分配设备编号范围的起始值,第二个参数请求连续的设备编号的数量,第三个参数为与设备范围关联的设备名称。函数头文件<linux/fs.h>
        if(ret<0)//该函数执行失败时返回小于0的值,成功执行后返回0,
        {
            printk(KERN_NOTICE "can not register tlv5618aid device");
            return ret;
        }
    
        cdev_init(&cdev_tlv5618aid,&s3c6410_tlv5618aid_fops);
        cdev_tlv5618aid.owner = THIS_MODULE;
    
        ret =cdev_add(&cdev_tlv5618aid,devno,1);
        if(ret)
        {
            printk(KERN_NOTICE "can not add tlv5618aid device");
            return ret;
        }
    
        my_class = class_create(THIS_MODULE,"my_class_01");
        //在/sys/class/下创建相对应的类目录
        if(IS_ERR(my_class))
        {
            printk("Err: Failed in creating class\n");
            return -1;  
        }
    
        device_create(my_class,NULL,MKDEV(TLV5618AID_MAJOR,0),NULL,DEVICE_NAME);//完成设备节点的自动创建,当加载模块时,就会在/dev下自动创建设备文件
        /* GPO5 pull up */
            tmp = __raw_readl(S3C64XX_GPOPUD);//读取原来GPIO的数据
            tmp &= (~0xc00);//先把位[2*5+1,2*5]清0,其他位不变tmp=tmp & 0x0011 1111 1111
            tmp |= 0x800;//再把位[2*5+1,2*5]的置为10,tmp=tmp | 0x1000 0000 0000上拉配置成功,
            __raw_writel(tmp,S3C64XX_GPOPUD);
    
            /* GPO5 output mode */
            tmp = __raw_readl(S3C64XX_GPOCON);
            tmp &= (~0xc00);//先把位[11,10]清0,其他位不变tmp=tmp & 0x0011 1111 1111
            tmp |= 0x400;//再把位[11,10]的置为01,tmp=tmp | 0x0100 0000 0000输出模式配置成功,
            __raw_writel(tmp,S3C64XX_GPOCON);
    
            /* GPO5 output 1 */
            tmp = __raw_readl(S3C64XX_GPODAT);
            tmp &= (~0x20);
            tmp |= 0x20;//输出高电平1,0x10 0000,第5位输出为1(从0开始计算)
            __raw_writel(tmp,S3C64XX_GPODAT);
    
        /* GPQ4 and GPQ6 pull up */
        tmp = __raw_readl(S3C64XX_GPQPUD);
        tmp &= (~0x3300);
        tmp |= 0x2200;
        __raw_writel(tmp,S3C64XX_GPQPUD);
    
        /* GPQ4 and GPQ6 output mode */
        tmp = __raw_readl(S3C64XX_GPQCON);
        tmp &= (~0x3300);
        tmp |= 0x1100;
        __raw_writel(tmp,S3C64XX_GPQCON);
    
        /* GPQ4 and GPQ6 output 1 */
        tmp = __raw_readl(S3C64XX_GPQDAT);
        tmp &= (~0x50);
        tmp |= 0x50;
        __raw_writel(tmp,S3C64XX_GPQDAT);
        //printk("S3C64XX_GPQCON is0x%08x\n",__raw_readl(S3C64XX_GPQCON));
        //printk("S3C64XX_GPQDAT is0x%08x\n",__raw_readl(S3C64XX_GPQDAT));
        //printk("S3C64XX_GPQPUD is0x%08x\n",__raw_readl(S3C64XX_GPQPUD));
        printk(DEVICE_NAME " initialized\n");
        return 0;
    }
    
    static void __exit s3c6410_tlv5618aid_exit(void)
    //与s3c6410_tlv5618aid_init()函数相对应,清除函数,在设备被移除前注销接口并向系统中返回所有资源
    {
        cdev_del(&cdev_tlv5618aid);
        //与函数cdev_add(&cdev_tlv5618aid,devno,1)相对应,从系统中移除设备
    //与register_chrdev_region()相对应,释放该设备编号的函数,第一个参数为分配设备编号范围的起始值,第二个参数请求连续的设备编号的数量
    
        device_destroy(my_class, MKDEV(TLV5618AID_MAJOR,0));//对应device_create(my_class,NULL,MKDEV(TLV5618AID_MAJOR,0),NULL,DEVICE_NAME);
        class_destroy(my_class);
        //对应class_create(THIS_MODULE,"my_class_01");
    
        printk(KERN_NOTICE "tlv5618aid_exit\n");
    }
    module_init(s3c6410_tlv5618aid_init);
    module_exit(s3c6410_tlv5618aid_exit);
    MODULE_LICENSE("GPL");

    三、Makefile部分

    makefile的编写可参考《LDD3》一书,源码如下

    obj-m := tlv5618aid.o
    //模块需要从目标文件tlv5618aid.o中构建
    KERNELDIR := /opt/htx-linux-2.6.xxxx
    //内核树的路径,为构造可装载的模块
    PWD := $(shell pwd)
    CROSS := /usr/local/arm/4.2.2-eabi/usr/bin/arm-linux-
    CC := $(CROSS)gcc
    default:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules 
    test:
            $(CC) -g -Wall tlv5618aid.c -o tlv5618aid
    clean:
            rm -fr *mod.c *.o *.ko modules.order Module.symvers

    四、测试程序部分

    源码如下:

    #include <unistd.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/ioctl.h>
    #include <signal.h>
    
    #define FID_CS 0x00
    #define VSK_CS 0x01
    
    int main(int argc, char *argv[])
    {
        int fd, ret;
        int cmd;
        char buf;
        short val;// change char to short ,由传输8bit数据变成16bit
        unsigned int tmp;
    
        if (argc == 1) {
                    printf("Usage: ./tlv5618_test [port] [data]    port:0--fid, 1--vsk.  data:0~4095\n");
                    exit(1);
            }
    
        if(sscanf(argv[1], "%d", &cmd) != 1 ) {
                    printf("Check  argument!\n");
                    exit(1);
            }
    
            if(cmd != 0 && cmd != 1) {
                    printf("Check the first argument!\n");
                    printf("Usage: ./tlv5618aid_test [port] [data]    port:0--fid, 1--vsk.  data:0~4095\n");
                    exit(1);
            }
    
        if(argc != 3) {
                    printf("Usage: ./tlv5618aid_test [port] [data]    port:0--fid, 1--vsk.  data:0~4095\n");
                    exit(1);
            }
    
        if((argv[2][0]=='0') && (argv[2][1]=='x')) {
                sscanf(argv[2], "0x%x", &tmp);
            val = tmp&0xFFF;//最大只能传输12bit数据
            } else
                    val = atoi(argv[2]);
    
        fd = open("/dev/tlv5618aid", O_RDWR);//change /dev/max5141
        if(fd<0) {
            perror("open device tlv5618aid:");
            exit(1);
        }
    
        if(cmd == 0) {
            ret = ioctl(fd, FID_CS, NULL);
            if(ret < 0) {
                perror("ioctl:");
                exit(1);
            }
            printf("fid = %d\n", val);
        } else if(cmd ==1) {
            ret = ioctl(fd, VSK_CS, NULL);
            if(ret < 0) {
                perror("ioctl:");
                exit(1);
            }
            printf("vsk = %d\n", val);
        }
    
        write(fd, &val, sizeof(val));
        close(fd);
        return 0;
    }

    五、代码Test

    1、驱动模块的加载

    把在linux上的tlv5618aid.c目录里make得到的tlv5618aid.ko文件拷贝至windows系统,再从windows通过串口上传在ARM开发板上,一般位于drivers目录下,把ko文件添加为可执行状态

    chmod +x tlv5618aid.ko
    insmod tlv5618aid.ko

    由于手里没有新版的tlv5618aid开发板,但之前测试该驱动功能都是OK的,因此还是用老版的max5141截图演示,原理都是一样的,后期有新版的板子我再更新截图。装载之后,在/dev设备目录下就会有该设备,这是device_create函数的作用,
    这里写图片描述
    以及用lsmod命令,会发现该模块成功加载:
    这里写图片描述

    2、测试程序的运行

    同样在linux可直接用arm-linux-gcc把测试程序tlv5618aid_test.c编译成可执行文件,我是在测试代码的目录下make的,其Makefile为:

    tlv5618aid: tlv5618aid_test.o
            /usr/local/arm/4.2.2-eabi/usr/bin/arm-linux-gcc  tlv5618aid_test.c -o tlv5618aid_test
    clean:
            rm *.o tlv5618aid_test -rf

    把可执行文件拷贝到ARM板的测试目录按命令格式执行:./tlv5618_test [port] [data],如图所示:
    这里写图片描述
    根据测试代码的编写即可把该功能添加到应用层界面QT。

    展开全文
  • USB设备驱动程序

    2011-12-08 11:32:32
    因为大部分USB外围设备由于功能性更适合流接口驱动的结构,所以一般都采用加载式流接口驱动程序模型来开发USB设备驱动程序。 (1)USB系统结构分析 WinCE下USB系统软件由两层组成:较高USB设备驱动程序层和较低的...
    
    

     一. USB加载式流接口驱动要点分析

    为了支持不同类型的外围设备,WinCE平台提供了具有定制接口的流接口驱动程序模型。因为大部分USB外围设备由于功能性更适合流接口驱动的结构,所以一般都采用加载式流接口驱动程序模型来开发USB设备驱动程序。

    (1)USB系统结构分析
    WinCE下USB系统软件由两层组成:较高USB设备驱动程序层和较低的USB函数层。较低的USB函数层本身又由两部分组成:较高的通用串行总线驱动程序(USBD)模块和较低的主控制器驱动程序(HCD)模块。通过HCD模块功能和USBD模块实现高层的USBD接口函数,USB设备驱动程序就能与外围设备进行通讯。

    在数据传输的过程中,操作流程通常按下列的次序进行:①USB设备驱动程序进行数据传输的初始化,即通过USBD接口函数给USBD模块发送数据传输的请求。②USBD模块将该请求分成一些单独的事务。③HCD模块排出事务次序。④主控制器硬件执行事务。这里需要提醒的是,所有的事务都是从主机发出的,外围设备完全是被动接受型的。

    (2)USB设备驱动程序入口点函数
    从结构分析我们可知,所有的USB设备驱动程序必须在它们的DLL库设置一定的入口点与USBD模块进行适当的交互。设置入口点函数有两个作用:一是使得 USBD 模块能与外部设备交互;二是使得驱动程序能创建和管理任何可能需要的注册键。

    下面简要介绍相关函数的作用:USBDeviceAttach是当 USB 设备连接到主计算机时运行,USBD模块会调用这个函数初始化USB设备,取得USB设备信息和配置USB设备,并且申请必需的资源。 USBInstallDrive是在第一次加载USB设备驱动程序时首先被调用,它使得驱动程序能创建需要的注册键,用于将一个驱动程序所需的注册表信息写入到HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers目录下,例如设备名称等。需要注意的是,USB设备驱动程序不使用标准的注册表函数,而是使用RegisterClientDriverID()、RegisterClientSettings()函数来注册相应的设备信息。

    USBUninstallDriver是在用户删除USB设备驱动程序时调用,负责删除注册键并释放其它相关资源。它通过调用 UnRegisterClientSettings()和UnRegisterClientDriverID()函数来删除由驱动程序的 USBInstallDriver()函数创建的所有注册键。因此,我们在驱动程序中就需要严格按照这三个函数的原型来实现,否则就不能为设备管理器所识别。

     

    二. USB设备驱动程序必须输出的函数有:
       

            ①USBDeviecAttach()。当USB设备连接到计算机上时,USBD模块就会调用此函数。这个函数主要用于初始化USB设备,取得USB设备信息,配置USB设备,并且申请必需的资源。
            ②USBInstallDriver ()。主要用于创建一个驱动程序加载所需的注册表信息,例如读/写超时、设备名称等。
            ③USBUninstallDriver ()。主要用于释放驱动程序所占用的资源,以及删除UsbInstallDriver ()函数创建的注册表等。
       

           上述3个函数接口是所有的USB驱动程序必须提供的,缺一不可。
       

           另外较为重要的是USB设备驱动程序的注册表配置。一般的USB设备驱动程序的注册表配置在[HKEY_LOCAL_MACHINE\Drivers\USB\LoadClients\口]下,每个驱动程序的子键都有Group 1_ID\Group2_ID\Group3_ID\DriverName 格式,设备的子键由供应商、设备类和协议信息通过下划线组成。表2列出了允许的组合。

     

           需要注意的是,注册表的构成都是以十进制数值来标识的,也要注意十进制和十六进制之间数的转换。

     

    三 、USB设备流接口驱动的实现步骤

    从WinCE USB设备驱动模型及结构分析中,我们可以清晰的看到主机和外设之间的实现方式。在主机端,通过USBD模块和HCD模块使用默认的PIPE访问一个通用的逻辑设备,实际上就是说USBD和HCD是一组访问所有USB设备的逻辑接口,它们负责管理所有USB设备的连接、加载、移除、数据传输和通用配置。其中HCD是主机控制驱动,是为USBD提供底层的功能访问服务,USBD是USB总线驱动,位于HCD的上层,利用HCD的服务提供较高层次的功能。因此,实现USB加载流驱动程序大致需要完成以下步骤:

    (1)选择代表设备的文件名前缀。前缀非常重要,设备管理器在注册表中通过前缀来识别设备。同时,在流接口命名时也将这个前缀作为入口点函数的前缀,如果设备前缀为XXX,那么流接口对应为XXX_Close,XXX_Init等。

    (2)设置驱动的各个入口点函数。所谓入口点是指提供给设备管理器的标准文件I/O接口。在生成一个DLL后,就用设备文件名前缀替换名字中的XXX。因此,每个加载式流接口驱动程序必须实现XXX_Init()、XXX_IOControl()以及XXX_PowerUp()等一组标准的函数,用来完成标准的文件I/O函数和电源管理等。

    (3)建立.DEF文件。当设备管理器初始化USB设备编译出来的流接口函数后,还必须建立一个.def文件。DEF文件定义了DLL要导出的接口集,而且加载式流驱动大多是以DLL形式存在的,所以应将DLL和DEF的文件名统一起来。DEF文件告诉链接程序需要输出什么样的函数,最后将驱动程序编译到内核中去,这样这个USB设备流接口驱动程序就可以被应用程序调用。

    (4)在注册表中为驱动程序建立表项。在注册表中建立驱动程序入口点,这样设备管理器才能识别和管理这个驱动。此外,注册表中还能存储额外的信息,这些信息可以在驱动运行之后被使用到。

    展开全文
  • 设备驱动程序共性

    千次阅读 2011-01-31 16:39:00
    设备驱动程序是一组内核例程的集合,它使得硬件设备响应控制设备的编程接口,最关键的是该接口是一组规范的VFS函数集(open, read, lseek, ioctl等等)。这些函数的实际实现由设备驱动程序全权负责。由于每个...

    设备驱动程序是一组内核例程的集合,它使得硬件设备响应控制设备的编程接口,最关键的是该接口是一组规范的VFS函数集(open, read, lseek, ioctl等等)。这些函数的实际实现由设备驱动程序全权负责。由于每个设备都有一个唯一的I/O控制器,因此就有唯一的命令和唯一的状态信息,所以大部分I/O设备都有自己的驱动程序。

     

    设备驱动程序的种类有很多。它们在对用户态应用程序提供支持的级别上有很大的不同,也对来自硬件设备的数据采集有不同的缓冲策略。这些选择极大地影响了设备驱动程序的内部结构,我们将在本节中重点进行分析。

     

    设备驱动程序并不仅仅由实现设备文件操作的函数组成。在使用设备驱动程序之前,有几个活动是肯定要发生的。

     

    1.1 设备驱动程序的注册

    前面提到,我们对设备文件发出的每个系统调用都由内核转化为对相应设备驱动程序的对应函数的调用。为了完成这个操作,设备驱动程序必须注册自己。换句话说,注册一个设备驱动程序意味着分配一个新的device_driver描述符,将其插入到设备驱动程序模型的数据结构中(也就是以kobject的形式表现在sysfs体系中),并把它与对应的设备文件(可能是多个设备文件)连接起来。如果设备文件对应的驱动程序以前没有注册,则对该设备文件的访问会返回错误码-ENODEV

     

    如果设备驱动程序被静态地编译进内核,则它的注册在内核初始化阶段进行。相反,如果驱动程序是作为一个内核模块来编译的,则它的注册在模块装入时进行。在后一种情况下,设备驱动程序也可以在模块卸载时注销自己。

     

    例如,我们考虑一个通用的PCI设备。为了能正确地对其进行处理,其设备驱动程序必须分配一个pci_driver类型的描述符,PCI内核层使用该描述符来处理设备。初始化描述符的一些字段后,设备驱动程序就会调用pci_register_driver()函数。事实上pci_driver描述符包括一个内嵌的device_driver描述符;pci_register_driver()函数仅仅初始化内嵌的驱动程序描述符中的字段,然后调用driver_register()函数把驱动程序插入设备驱动程序模型的据结构中。

     

    注册设备驱动程序时,内核会寻找可能由该驱动程序处理但还尚未获得支持的硬件设备。为了做到这点,内核主要依靠相关的总线类型描述符bus_typematch方法,以及device_driver对象的probe方法。如果探测到可被驱动程序处理的硬件设备,内核会分配一个设备对象,然后调用device_register()函数把设备插入设备驱动程序模型中。

     

    1.2 初始化设备驱动程序

    注意,对设备驱动程序进行注册和初始化是两回事。设备驱动程序应当尽快被注册,以便用户态应用程序能通过相应的设备文件使用它。相反,设备驱动程序在最后可能的时刻才被初始化。事实上,初始化驱动程序意味着分配宝贵的系统资源,那么这些资源因此就对其他驱动程序不可用了。

     

    例如IRQ资源:把IRQ分配给设备通常是自动进行的,这正好发生在使用设备之前,因为多个设备可能共享同一条IRQ线。还有一些可以在最后时刻被分配的资源如内存中用于DMA传送缓冲区的页框和DMA通道本身(用于像软盘驱动器那样的老式非PCI设备)。

     

    为了确保资源在需要时能够获得,在获得后不再被其他驱动程序请求,设备驱动程序通常采用下列模式进行初始化和释放:

    1.         引用计数器记录当前访问设备文件的进程数。在设备文件的open方法中计数器被增加,在release方法中被减少。

    2.         open方法在增加引用计数器的值之前先检查它。如果计数器为0,则设备驱动程序必须分配资源并激活硬件设备上的中断和DMA

    3.         release方法在减少使用计数器的值之后检查它。如果计数器为0,说明已经没有进程使用这个硬件设备。如果是这样,该方法将禁止I/O控制器上的中断和DMA,然后释放所分配的资源。

     

    1.3 监控I/O操作

    I/O操作的监控是一项非常重要的工作,因为I/O操作的持续时间通常是不可预知的。这可能和机械装置的情况有关(对于要传送的数据块来说是磁头的当前位置),和实际的随机事件有关(数据包什么时候到达网卡),还和人为因素有关(用户在键盘上按下一个键或者发现打印机夹纸了)。在任何情况下,启动I/O操作的设备驱动程序都必须依靠一种监控技术在I/O操作终止或超时时发出信号。

     

    在终止操作的情况下,设备驱动程序读取I/O接口状态寄存器的内容来确定I/O操作是否成功执行。在超时的情况下,驱动程序知道一定出了问题,因为完成操作所允许的最大时间间隔已经用完,但什么也没做。

     

    监控I/O操作结束的两种可用技术分别称为轮询模式(polling mode)和中断模式(interrupt mode)。

     

    1.3.1 轮询模式

    CPU依照这种技术重复检查(轮询)设备的状态寄存器,直到寄存器的值表明I/O操作已经完成为止。Linux的“自旋锁”机制就是一种基于轮询的技术:当处理器试图获得一个繁忙的自旋锁时,它就重复地查询变量的值,直到该值变成0为止。但是,应用到I/O操作中的轮询技术更加巧妙,这是因为驱动程序还必须记住检查可能的超时。下面是轮询的一个简单例子:

     

        for (;;) {

            if (read_status(device) & DEVICE_END_OPERATION) break;

            if (--count == 0) break;

        }

     

    在进入循环之前,count变量已被初始化,每次循环都对count的值减1,因此就可以使用这个变量实现一种粗略的超时机制。另外,更精确的超时机制可以通过这样的方法实现:在每次循环时读取节拍计数器jiffies的值,并将它与开始等待循环之前读取的原值进行比较。

     

    如果完成I/O操作需要的时间相对较多,比如说毫秒级,那么这种模式就变得低效,因为CPU花费宝贵的机器周期去等待I/O操作的完成。在这种情况下,在每次轮询操作之后,可以通过把schedule()的调用插入到循环内部来自愿放弃CPU

     

    1.3.2中断模式

    使用中断模式有个前提,那就是I/O控制器必须能够通过IRQ线发出I/O操作结束的信号,这样,中断模式才能被使用。

     

    我们现在通过一个简单的例子说明中断模式如何工作。假定我们想实现一个简单的输入字符设备的驱动程序。当用户在相应的设备文件上发出read()系统调用时,一条输入命令被发往设备的控制寄存器。在一个不可预知的长时间间隔后,设备把一个字节的数据放进输入寄存器,并通过I/O控制器发送一个中断。设备驱动程序然后将这个字节作为read()系统调用的结果返回。

     

    这是一个用中断模式实现驱动程序的典型例子。实质上,我们这个例子中的设备的驱动程序就只包含两个函数:

    1.      实现文件对象read方法的foo_read()函数。

    2.      处理中断的foo_interrupt()函数。

     

    只要用户读设备文件,foo_read()函数就被触发:

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

        {/* 参数filp是设备文件,buf是输入数据缓存,count是输入数据长度,ppos当前位置

            foo_dev_t * foo_dev = filp->private_data;

            if (down_interruptible(&foo_dev->sem)

                return -ERESTARTSYS;

            foo_dev->intr = 0;

            outb(DEV_FOO_READ, DEV_FOO_CONTROL_PORT);

            wait_event_interruptible(foo_dev->wait, (foo_dev->intr = =1));

            if (put_user(foo_dev->data, buf))

                return -EFAULT;

            up(&foo_dev->sem);

            return 1;

        }

     

    设备驱动程序依赖类型为foo_dev_t的自定义描述符;它包含信号量sem(保护硬件设备免受并发访问)、等待队列wait、标志intr(当设备发出一个中断时设置)及单个字节缓冲区data(由中断处理程序写入且由read方法读取)。一般而言,所有使用中断的I/O驱动程序都依赖中断处理程序及readwrite方法均访问的数据结构。foo_dev_t描述符的地址通常存放在设备文件的文件对象的private_data字段中或一个全局变量中。

     

    foo_read()函数的主要操作如下:

    1.         获取foo_dev->sem信号量,因此确保没有其他进程访问该设备。

    2.         intr标志。

    3.         I/O设备发出读命令。

    4.         执行wait_event_interruptible以挂起进程,直到intr标志变为1

     

    一定时间后,我们的设备发出中断信号以通知I/O操作已经完成,数据已经放在适当的DEV_FOO_DATA_PORT数据端口。中断处理程序置intr标志并唤醒进程。当调度程序决定重新执行这个进程时,foo_read()的第二部分被执行,步骤如下:

    1.       把准备在foo_dev->data变量中的字符拷贝到用户地址空间。

    2.       释放foo_dev->sem信号量后终止。

     

    为了简单起见,我们没有包含任何超时控制。一般来说,超时控制是通过静态或动态定时器实现的;定时器必须设置为启动I/O操作后正确的时间,并在操作结束时删除。

     

    让我们来看一下foo_interrupt()函数的代码:

        irqreturn_t foo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

        {

            foo->data = inb(DEV_FOO_DATA_PORT);

            foo->intr = 1;

            wake_up_interruptible(&foo->wait);

            return 1;

        }

     

    中断处理程序从设备的输入寄存器中读字符,并把它存放在foo全局变量指向的驱动程序描述符foo_dev_tdata字段中。然后设置intr标志,并调用wake_up_interruptible()函数唤醒在foo->wait等待队列上阻塞的进程。

     

    注意,三个参数中没有一个被中断处理程序使用,这是其实是相当普遍的情况。

     

    1.4 访问I/0共享存储器

    根据设备和总线的类型,现代PC体系结构里的I/O共享存储器可以被映射到不同的物理地址范围。主要有:

     

    对于连接到ISA,总线上的大多数没备

    I/O共享存储器通常被映射到Oxa0000Oxfffff16位物理地址范围;这就在640 KB1 MB之间留出了一段空间。

     

    对子连接到PCI,总线上的设备

    I/O共享存储器被映射到接近4 GB32位物理地址范围。这种类型的设备更加容易处理。

     

    I/O共享存储器机制是极其重要的,因为建立好这些映射以后,访问设备接口中的存储器如同访问内存一样简单,就不需要那么多纷繁复杂的I/O交换了,大大提升了系统I/O处理的效率。

     

    几年以前,Intel引入了图形加速端口(AGP)标准,该标准是适合于高性能图形卡的PCI的增强。这种卡除了有自己的I/O共享存储器外,还能够通过图形地址再映像表(GART)这个特殊的硬件电路直接对主板的RAM部分进行寻址。GART电路能够使AGP卡比老的PCI卡具有更高的数据传输速率。然而,从内核的观点看,物理存储器位于何处根本没有什么关系,GART映射的存储器与其他种类I/O共享存储器的处理方式完全一样。

     

    设备驱动程序如何访问一个I/O共享存储器单元?让我们从比较简单的PC体系结构开始人手,之后再扩展到其他体系结构。

     

    不要忘了内核程序作用于线性地址,因此I/O共享存储器单元必须表示成大于PAGE_OFFSET的地址,这样,才有利于对I/O共享存储器单元的物理地址进行映射。我们假设PAGE_OFFSET等于Oxc0000000(在x8632为体系中,实际上也是这样干的),也就是说,内核线性地址是在第4GB

     

    设备驱动程序必须把I/O共享存储器单元的物理地址转换成内核空间的线性地址。在PC体系结构中,这可以简单地把32位的物理地址和Oxc0000000常量进行或运算得到。例如,假设内核需要把物理地址为Ox000b0fe4I/O单元的值存放在t1中,把物理地址为Oxfc000000I/O单元的值存放在t2中。你可能认为使用下面的表达式就可以完成这项工作:

        t1 = *((unsigned char *)(0xc00b0fe4));

        t2 = *((unsigned char *)(0xfc000000));

     

    在初始化阶段,内核已经把可用的RAM物理地址映射到线性地址空间第4GB的开始部分。因此,分页单元把出现在第一个语句中的线性地址OXCOObOfe4映射回到原来的I/O物理地址OXOOObOfe4,这正好落在从640KBIMB的这段“ISA洞”中。这工作得很好。

     

    但是,对于第二个语句来说,这里有一个问题,因为其I/O物理地址超过了系统RAM的最大物理地址。因此,线性地址Oxfc000000就不需要与物理地址Oxfc000000相对应。在这种情况下,为了在内核页表中包括对这个I/O物理地址进行映射的线性地址,必须对页表进行修改。这可以通过调用ioremap()ioremap_nocache()函数来实现,第一个函数与vmalloc()函数类似,都调用get_vm_area()为所请求的I/O共享存储区的大小建立一个新的vm_struct描述符。然后,这两个函数适当地更新常规内核页表中的对应页表项。ioremap_nocache()不同于ioremap(),因为前者在适当地引用再映射的线性地址时还使硬件高速缓存内容失效。

     

    因此,第二个语句的正确形式应该为:

        io_mem = ioremap(0xfb000000, 0x200000);

        t2 = *((unsigned char *)(io_mem + 0x100000));

     

    第一条语句建立一个2MB的新的线性地址区间,该区间映射了从Oxfb000000开始的物理地址,第二条语句读取地址为Oxfc000000的内存单元。设备驱动程序以后要取消这种映射,就必须使用iounmap()函数。

     

    在其他体系结构(PC之外的体系结构)上,简单地间接引用物理内存单元的线性地址并不能正确访问I/O共享存储器。因此,Linux定义了下列依赖于体系结构的函数,当访问I/O共享存储器时来使用它们:

    readb( ), readw( ), readl( )

             分别从一个I/O共享存储器单元读取12或者4个字节

    writeb( ), writew( ), writel( )

             分别向一个I/O共享存储器单元写入12或者4个字节

    memcpy_fromio( ), memcpy_toio( )

             把一个数据块从一个I/O共享存储器单元拷贝到动态内存中,另一个函数正好相反

    memset_io( )

             用一个固定的值填充一个I/O共享存储器区域

     

    最后,对于Oxfc000000 I/O单元的访问推荐使用这样的方法:

        io_mem = ioremap(0xfb000000, 0x200000);

        t2 = readb(io_mem + 0x100000);

     

    正是由于这些函数,就可以隐藏不同平台访问I/O共享存储器所用方法的差异。

     

    1.5 直接存储访问机制(DMA

    在最初的PC体系结构中,CPU是系统中唯一的总线主控器,也就是说,为了提取和存储RAM存储单元的值,CPU是唯一可以驱动地址/数据总线的硬件设备。随着更多诸如PCI这样的现代总线体系结构的出现,如果提供合适的电路,每一个外围设备都可以充当总线主控器。因此,现在所有的PC都包含一个辅助的DMA电路,它可以用来控制在RAMI/O设备之间数据的传送。DMA一旦被CPU激活,就可以自行传送数据;当数据传送完成之后,DMA发出一个中断请求。当CPUDMA同时访问同一内存单元时,所产生的冲突由一个名为内存仲裁器的硬件电路来解决。

     

    使用DMA最多的是磁盘驱动器和其他需要一次传送大量字节的设备。因为DMA的设置时间相当长,所以在传送数量很少的数据时直接使用CPU效率更高。

     

    原来的ISA总线所使用的DMA电路非常复杂,难于对其进行编程,并且限于物理内存的低16M8PCISCSI总线所使用的最新DMA电路依靠总线中的专用硬件电路,这就简化了设备驱动程序开发人员的开发工作。

     

    1.5.1 同步DMA和异步DMA

    设备驱动程序可以采用两种方式使用DMA,分别是同步DMA和异步DMA。第一种方式,数据的传送是由进程触发的;而第二种方式,数据的传送是由硬件设备触发的。

     

    采用同步DMA传送的例子如声卡,它可以播放电影音乐。用户态应用程序将声音数据(称为样本)写入一个与声卡的数字信号处理器(DSP)相对应的设备文件中。声卡的驱动程序把写入的这些样本收集在内核缓冲区中。同时,驱动程序命令声卡把这些样本从内核缓冲区拷贝到预先定时的DSP中。当声卡完成数据传送时,就会引发一个中断,然后驱动程序会检查内核缓冲区是否还有要播放的样本;如果没有,驱动程序就再启动一次DMA数据传送。

     

    采用异步DMA传送的例子如网卡,它可以从一个LAN中接收帧(数据包)。网卡将接收到的帧存储在自己的I/O共享存储器中,然后引发一个中断。其驱动程序确认该中断后,命令网卡将接收到的帧从I/O共享存储器拷贝到内核缓冲区。当数据传送完成后,网卡会引发新的中断,然后驱动程序将这个新帧通知给上层内核层。

     

    1.5.2 DMA传送的辅助函数

    当为使用DMA传送方式的设备设计驱动程序时,开发者编写的代码应该与体系结构和总线(就DMA传送方式来说)二者都不相关。由于内核提供了丰富的DMA辅助函数,因而现在上述目标是可以实现的。这些辅助函数隐藏了不同硬件体系结构的DMA实现机制的差异。

     

    这是DMA辅助函数的两下子集:老式的子集为PCI设备提供了与体系结构无关的函数;新的子集则保证了与总线和体系结构两者都无关。我们现在将介绍其中的一些函数,同时指出DMA的一些硬件特性。

     

    1、总线地址

     

    DMA的每次数据传送(至少)需要一个内存缓冲区,它包含硬件设备要读出或写入的数据。一般而言,启动一次数据传送前,设备驱动程序必须确保DMA电路可以直接访问RAM内存单元。

     

    到现在为止,我们已区分了三类存储器地址:逻辑地址、线性地址以及物理地址,前两个在CPU内部使用,最后一个是CPU利用硬件分页机制从物理上驱动数据总线所用的内存RAM物理地址。但是,还有第四种存储器地址,称为总线地址(bus address),它是除CPU之外的硬件设备驱动数据总线时所用的存储器地址。

     

    从根本上说,内核为什么应该关心总线地址呢?这是因为在DMA操作中,数据传送不需要CPU的参与;I/O设备和DMA电路直接驱动数据总线(注意,这里有可能会跟CPU产生竞争数据总线的情况)。因此,当内核开始DMA操作时,必须把所涉及的内存缓冲区总线地址要么写人DMA适当的I/O端口,要么写人I/O设备适当的I/O端口。

     

    80x86体系结构中,总线地址与物理地址是一致的,也就是前面我们谈到的I/O共享存储。然而,其他的体系结构例如Sun公司的SPARCHPAlpha都包括一个所谓的I/O存储器管理单元(IO-MMU)的硬件电路,它类似于微处理器的分页单元,将物理地址映射为总线地址。使用DMA的所有I/O驱动程序在启动一次数据传送前必须设置好IO-MMU

     

    不同的总线具有不同的总线地址大小。例如,ISA的总线地址是24位长,因此,在80x86体系结构中,可以在物理内存的低16 MB中完成DMA传送——这就是为什么DMA使用的内存缓冲区分配在ZONE_DMA内存区中(设置了GFP_DMA标志)。再来看看PCI,原来的PCI标准定义了32位的总线地址;但是,一些PCI硬件设备最初是为ISA总线而设计的,因此它们仍然访问不了物理地址Ox00ffffff以上的RAM内存单元。新的PCI-E标准采用64位的总线地址并允许DMA电路可以直接寻址更高的内存。

     

    Linux中,数据类型dma_addr_t代表一个通用的总线地址。在80x86体系结构中,dma_addr_t对应一个32位长的整数,除非内核支持PAE,在这种情形下,dma_addr_t代表一个64位的整数。

     

    pci_set_dma_mask()dma_set_mask()两个辅助函数用于检查总线是否可以接收给定大小的总线地址(mask),如果可以,则通知总线层给定的外围设备将使用该大小的总线地址。

     

    2、高速缓存的一致性

     

    高速缓存一致性问题,类似于内核同步访问机制,系统体系结构没有必要在硬件级为硬件高速缓存与DMA电路之间提供一个一致性协议,因此,执行DMA映射操作时,DMA辅助函数必须考虑硬件高速缓存。为了弄清楚这是为什么,假设设备驱动程序把一些数据填充到内存缓冲区中,然后立刻命令硬件设备利用DMA传送方式读取该数据。如果DMA访问这些物理RAM内存单元,而相应的硬件高速缓存行的内容还没有写入RAM中,那么硬件设备所读取的值就是内存缓冲区中的旧值。

     

    设备驱动程序开发人员可以采用两种方法来处理DMA缓冲区,他们分别使用两类不同的辅助函数来完成。用Linux的术语来说,开发人员在下面两种DMA映射类型中进行选择:

     

    一致性DMA映射

    使用这种映射方式时,内核必须保证内存与硬件设备间高速缓存一致性不是什么问题;也就是说CPURAM内存单元上所执行的每个写操作对硬件设备而言都是立即可见的,反过来也一样。这种映射方式也称为“同步的”或“一致的”。

     

    流式DMA映射

    使用这种映射方式时,设备驱动程序必须了解高速缓存一致性问题,这可以使用适当的同步辅助函数来解决。这种映射方式也称为“异步的”或“非一致性的”。

     

    80x86体系结构中使用DMA时,不存在高速缓存一致性问题,因为硬件设备驱动程序本身会“窥探”所访问的硬件高速缓存。因此,80x86体系结构中为硬件设备所设计的驱动程序会从前述的两种DMA映射方式中选择一个:它们二者在本质上是等价的。另一方面,在诸如MIPSSPARC以及PowerPC的一些模型等许多其他的体系结构中,硬件设备通常不窥探硬件高速缓存,因而就会产生高速缓存一致性问题。总的来说,为与体系结构无关的驱动程序选择一个合适的DMA映射方式是很重要。

     

    一般来说,如果CPUDMA处理器以不可预知的方式去访问一个缓冲区,那么必须强制使用一致性DMA映射方式(例如,SCSI适配器的command数据结构的缓冲区)。其他情形下,流式DMA映射方式更可取,因为在一些体系结构中处理一致性DMA映射是很麻烦的,并且可能导致更低的系统性能。

     

    3、一致性DMA映射的辅助函数

     

    通常,设备驱动程序在初始化阶段会分配内存缓冲区并建立一致性DMA映射;在卸载时释放映射和缓冲区。为了分配内存缓冲区和建立一致性DMA映射,内核提供了依赖体系结构的pci_alloc_consistent()dma_alloc_coherent()两个函数。它们均返回新缓冲区的线性地址和总线地址。在80x86体系结构中,它们返回新缓冲区的线性地址和物理地址。为了释放映射和缓冲区,内核提供了pci_free_consistent()dma_free_coherent()两个函数。

     

    4、流式DMA映射的辅助函数

     

    流式DMA映射的内存缓冲区通常在数据传送之前被映射,在传送之后被取消映射。他有可能在几次DMA传送过程中保持相同的映射,但是在这种情况下,设备驱动程序开发人员必须知道位于内存和外围设备之间的硬件高速缓存。

     

    为了启动一次流式DMA数据传送,驱动程序必须首先利用分区页框分配器或通用内存分配器来动态地分配内存缓冲区。然后,驱动程序调用pci_map_single()函数或者dma_map_single()函数建立流式DMA映射,这两个函数接收缓冲区的线性地址作为其参数并返回相应的总线地址。为了释放该映射,驱动程序调用相应的pci_unmap_single()函数或dma_unmap_single()函数。

     

    为了避免高速缓存一致性问题,驱动程序在开始从RAM到设备的DMA数据传送之前,如果有必要,应该调用pci_dma_sync_single_for_device()函数或dma_sync_single_for_device()函数刷新与DMA缓冲区对应的高速缓存行。同样地,从设备到RAM的一次DMA数据传送完成之前设备驱动程序是不可以访问内存缓冲区的:相反,如果有必要,在读缓冲区之前,驱动程序应该调用pci_dma_sync_single_for_cpu()函数或dma_sync_single_for_cpu()函数使相应的硬件高速缓存行无效。在80x86体系结构中,上述函数几乎不做任何事情,因为硬件高速缓存和DMA之间的一致性是由硬件来维护的。

     

    即使是高端内存的缓冲区也可以用于DMA传送;开发人员使用pci_map_page()dma_map_page()函数,给其传递的参数为缓冲区所在页的描述符地址和页中缓冲区的偏移地址。相应地,为了释放高端内存缓冲区的映射,开发人员使用pci_unmap_page()dma_unmap_page()函数。

     

    1.6 内核支持级别

    最后,我们来总结一下内核对I/O设备的支持级别。Linux内核并不完全支持所有可能存在的I/O设备。一般来说,有三种可能的方式支持硬件设备:

     

    压根就不支持:

    应用程序使用适当的inout汇编语言指令直接与设备的I/O端口进行交互。

     

    最小支持:

    内核不识别硬件设备,但能识别它的I/O接口。用户程序把I/O接口视为能够读写字符流的顺序设备。

     

    扩展支持:

    内核识别硬件设备,并处理I/O接口本身。不过,这种设备可能没有对应的设备文件。

     

    展开全文
  • 在第八章“硬件管理”中,我们介绍了...不过,基本重点是访问PCI外围的核心功能,因为近来,PCI总线是最常用的外围总线,也是核心支持最好的总线。 PCI接口 尽管很多计算机用户认为PCI(外围部件互连,Peripheral Com

    在第八章“硬件管理”中,我们介绍了最低级的硬件控制,本章提供一个较高级的总线体系结构的概览。总线由电气接口和编程接口组成。在这一章,我打算介绍编程接口。

    本章覆盖了几种总线体系结构。不过,基本重点是访问PCI外围的核心功能,因为近来,PCI总线是最常用的外围总线,也是核心支持最好的总线。

     

    PCI接口

     

    尽管很多计算机用户认为PCI(外围部件互连,Peripheral Component Interconnect)是布局电气线路的一种方法,但实际上,它是一组完全的规范,定义了计算机的不同部分是如何交互的。

    PCI规范覆盖了与计算机接口相关的绝大多数方面。我不打算在这里全部介绍,在本节中,我主要关心一个PCI驱动程序是如何找到它的硬件,并获得对它的访问的。在第二章“构造和运行模块”的“自动和手工配置”一节,及在第九章“中断处理”的“自动检测中断号”一节中讨论过的探测技术同样可以应用于PCI设备,但规范还提供了探测的另外办法。

    PCI结构被设计来替代ISA标准,由三个主要目标:在计算机和其外围之间传送数据时有更高的性能,尽可能地做到平台无关性,使在系统中增减外围设备得到简化。

    PCI通过使用比ISA高的时钟频率来获得更高的性能;它的时钟运行在2533MHZ(实际时钟是系统时钟的几分之一的整数倍),而且马上就会游66MHZ的扩展。另外,它被装配在32位的数据总线上,64位的扩展正在规范中。平台无关性一直是计算机总线的一个设计目标,这是PCI的尤其重要的一个特征,因为PC世界一直以来总是被处理器特定的标准所主宰。

    不过对驱动程序作者来说,最要紧的是对接口板自动检测的支持。PCI设备是无跳线的(与大多数ISA外围不同),并且在引导时被自动配置。因此,设备驱动程序必须能访问设备上的配置信息来完成初始化。这些情形都不需要任何探测。

     

    PCI寻址

    每个外围由一个总线号、一个设备号、和一个功能号确定。虽然PCI规范允许一个系统最多拥有256条总线,但PC只有一条。每条总线最多带32个设备,但每个设备可以是最多个功能的多功能板(如一个音频设备带一个CD-ROM驱动器)。每个功能可以由一个16位的键或两个8位的键确定。Linux核心采用后一种方法。

    每个外围板子的硬件电路回答与三个地址空间相关的询问:内存位置,I/O端口,和配置寄存器。前两个地址空间由PCI总线上的所有设备共享(也就是说,当你访问一个内存位置,所有的设备都将同时看到这个总线周期)。而配置空间则利用“地理寻址”,每个槽有一个配置事务的私用使能线,PCI控制器一次访问一个板子,不会有地址冲突。考虑到驱动程序,内存和I/O是以通常的inb,memcpy等来访问。而配置事务则通过调用特定的核心函数访问配置寄存器来完成。至于中断,每个PCI设备有4个中断管脚,它们到处理器中断线的路由是主板的任务;PCI中断可以设计为共享的,这样即使是一个有限中断线的处理器也能带很多PCI接口板。

    PCI总线的I/O空间使用32位的地址总线(这样就是4GBI/O端口),而内存空间则可以用32位或64位地址访问。地址对每个设备来说应该是唯一的,但也有可能有两个设备错误地映射到同一个地址,使得哪个都不能被访问。一个好消息是接口卡提供的每个内存和I/O地址区段都可以通过配置事务重映射。这就是设备可以在引导时被初始化从而避免地址冲突的机制这些区段当前映射到的地址可以从配置空间读出,因此Linux驱动程序可以不通过探测就访问其设备。一旦配置寄存器被读出,驱动程序就可以安全的访问它的硬件。

    PCI配置空间由每个设备函数256个字节构成,配置寄存器的布局是标准化的。配置空间有四个字节含有一个唯一的函数ID,因此驱动程序可以通过在外围查找特定的IDB确定它的设备*。总之,每个设备板子被地理寻址以取得它的配置寄存器;这个信息可以用来确定这个板子或采取进一步动作。

    从前面的描述,应该清楚PCI接口标准比ISA的主要创新是配置地址空间。因此,除了通常的驱动程序代码外,PCI驱动程序还需要访问配置空间的能力。

    在本章的其余部分,我将使用单词“设备”来指一个设备功能,因为多功能板上的每个功能均是一个独立的实体。当我提到一个设备,我是指元组“总线号,设备号,功能号”。如前所述,每个元组在Linux中由两个8位数字表示。

     

    引导时

    让我们看一下PCI是如何工作的,从系统引导开始,因为那时设备被配置。

    PCI设备被加电时,硬件关闭。或者说,设备只对配置事务响应。加电时,设备没有映射到计算机地址空间的内存和I/O端口;所有其它的设备特定的特征,象中断线,也都被关闭。

    幸运的是,每个PCI母板都装有懂得PCI 的固件,根据平台的不同被称做BIOSNVRAM、或PROM。固件提供对设备配置地址空间的访问,即使处理器的指令集不提供这样的能力。

    在系统引导时,固件对每个PCI外围执行配置事务,从而为它提供的任何地址区段分配一个安全的地方。到设备驱动程序访问设备时,它的内存和I/O区段已经被映射到处理器的地址空间。驱动程序可以改变这个缺省的分配,但它通常并不这样做,除非有一些设备相关的原因要求这样。

    Linux中,用户可以通过读/proc/pci来查看PCI 设备,这是个文本文件,系统中每个PCI板子有一项。下面是/proc/pci中一项的例子:

    (代码344

    /proc/pci中每一项是一个设备的设备无关特征的概述,如它的配置寄存器所描述的。例如,上面这一项告诉我们这个设备有板上内存,已被映射到地址0xf1000000。一些古怪的细节的含义以后在我介绍过配置寄存器后将会清楚。

     

    检测设备

    如前面提到的,配置空间的布局是设备无关的。在这一节,我们将看看用来确定外围的配置寄存器。

    PCI设备有一个256字节的地址空间。前64个字节是标准化的,而其余的则是设备相关的。图15-1显示了设备无关配置空间的布局。

    如图所示,有些PCI的配置寄存器是要求的,而有些则是可选的。每个PCI设备必须在必要寄存器中包含有意义的值,而可选寄存器的内容则以来与实际外围的能力。可选域并不使用,除非必要域的内容表明它们是有效的。这样,必要域断言了板子的能力,包括其它域可用与否。

    有意思的是注意到PCI寄存器总是小印地安字节顺序的。尽管标准要设计为体系结构无关的,PCI的设计者有时还是显示出对PC环境的偏见。驱动程序的作者应该留神字节顺序,特别是访问多字节的配置寄存器时;在PC上工作的代码可能在别的平台上就不行。Linux的开发者已经注意了字节排序问题(见下一节“访问配置空间”),但这个问题还是要牢记在心。不幸的是,标准函数ntohsntohl都不能用,因为网络字节顺序与PCI顺序相反;在Linux2.0中没有标准函数将PCI字节顺序转换为主机字节顺序,每个用单个字节构成多字节值的驱动程序都应该特别小心地正确处理印地安字节序。核心版本2.1.10引入了几个函数来处理这些字节顺序问题,它们在第十七章“最近的发展”中“转换函数”一节介绍。

    (图15-1:标准化的PCI配置寄存器)

    描述所有的配置项超出了本书的范围。通常,与设备一起发布的技术文档会描述它支持的寄存器。我们感兴趣的是驱动程序如何找到它的设备,以及它如何访问设备的配置空间。

    三个PCI寄存器确定一个设备:销售商,设备ID,和类。每个PCI外围把它自己的值放入这些只读寄存器,驱动程序可以用它们来查找设备。让我们更仔细地看看这些寄存器:

    销售商

    这个16位的寄存器确定硬件的生产商。例如,每个Intel的设备都会标上同样的销售商号,8086 hex(是个随即值?)。这样的号码有一个全球的注册,生产商必须申请一个唯一的号。

    设备ID

    这是另一个16位寄存器,由生产商选择;不需要有官方的注册。这个ID通常与销售商ID成对出现,形成一个硬件设备的唯一的32位标志符。我将用单词“签名”来指销售商/设备ID对。一个设备驱动程序经常以来于签名来确定它的设备;驱动程序的作者从硬件文档中知道要寻找什么值。

    每个外围设备都属于一个类。类寄存器是个16位的值,它的高八位确定 “基类”(或组)。例如,“以太网”和“令牌环”是属于“网络”组的两类,而“串行”和“并行”类属于“通信”组。有些驱动程序可以支持几种类似的设备,它们虽然有不同的签名,却属于同一类;这些驱动程序可以依赖于类寄存器来确定它们的外围,如以后所示。

     

    下面的头文件,宏,以及函数都将被PCI驱动程序用来寻找它的硬件设备:

    #include <linux/config.h>

    驱动程序需要知道是否PCI函数在核心是可用的。通过包含这个头文件,驱动程序获得了对CONFIG_宏的访问,包括CONFIG_PCI(将在下面介绍)。从1.3.73以来,这个头文件包含在<linux/fs.h>中;如果想向后兼容,你必须把它显式地包含。

    CONFIG_PCI

    如果核心包括对PCI BIOS调用的支持,那么这个宏被定义。并不是每个计算机都有PCI总线,所以核心的开发者应该把     PCI的支持做成编译时选项,从而在无PCI的计算机上运行Linux时节省内存。如果CONFIG_PCI没有定义,那么这个列表中其它的函数都不可用,驱动程序应使用预编译的条件语句将PCI支持全都排除在外,以避免加载时的“未定义符号”错。

    #include <linux/bios32.h>

    这个头文件声明了本节介绍的所有的原型,因此一定要被包含。这个头文件还定义了函数返回的错误代码的符号值。它在1.22.0之间没有改变,因此没有可移植性问题。

    int pcibios_present(void)

    由于PCI相关的函数在无PCI的计算机上毫无意义,pcibios_present函数就是告诉驱动程序计算机是否支持PCI;如果BIOS懂得PCI,它返回一个为真布尔值。即使CONFIG_PCI被定义了,PCI功能仍是一个运行时选项。因此,你在调用下面介绍的函数之前要检查一下pcibios_present,保证计算机支持PCI

    #include <linux/pci.h>

    这个头文件定义了下面函数使用的所有数值的符号名。并不是所有的设备ID都在这个文件中列出了,但你在为你的ID,销售商,类定义宏之前,最好还是看看这个文件。注意这个文件一直在变大,因为不断有新设备的符号定义被加入。

    int pcibios_find_device(unsigned short vendor, unsigned short id, unsigned short index,

                       unsigned char *bus, unsigned char *function);

    如果CONFIG_PCI被定义了,并且pcibios_present也是真,这个函数被用来从BIOS请求关于设备的信息。销售商/ID对确定设备。index用来支持具有同样的销售商/ID对的几个设备,下面将会解释。对这个函数的调用返回设备在总线上的位置以及函数指针。返回代码为0表示成功,非0表示失败。

    int pcibios_find_class(unsigned int class_code, unsigned short index,

                      unsigned char *bus, unsigned char *function);

    这个函数和上一个类似,但它寻找属于特定类的设备。参数class_code传递的形式为:16位的类寄存器左移八位,这与BIOS接口使用类寄存器的方式有关。这次还是,返回代码为0表示成功,非0表示有错。

    char *pcibios_strerror(int error);

    这个函数用来翻译一个PCI错误代码(象pcibios_find_device返回的)为一个字符串。你也许在查找函数返回的即不是PCIBIOS_SUCCESSFUL(0),也不是PCIBIOS_DEVICE_NOT_FOUND时(这是当所有的设备都被找过以后所期望返回的错误代码),希望打印一条错误信息。

     

    下面的代码是驱动程序在加载时检测设备所使用的典型代码。如上面所提到的,这个查找可以基于签名或者设备类。不管是哪种情况,驱动程序不许存储busfunction值,它们在后面确定设备时要用到。function的前五位确定设备,后三位确定函数。

    下面的代码中,每个设备特定的符号加前缀jail_(另一个指令列表),大写或小写依赖于符号的种类。

    如果驱动程序可以依赖于唯一的销售商/ID对,下面的循环可以用来初始化驱动程序:

    (代码347

    (代码348

    如果这个代码段只处理由JAIL_VENDORJAIL_ID确定的一类PCI设备,那么它是正确的。

    不过,很多驱动程序非常灵活,能够同时处理PCIISA板子。在这种情况下,驱动程序仅在没有检测到PCI板子或CONFIG_PCIBIOS没有定义时才探测ISA设备。

    使用pcibios_find_class要求jail_init_dev完成比例子中要多的工作。只要它找到了一个属于指定类的设备,这个函数就成功返回,但驱动程序还要确认其签名也是被支持的。这个任务通过一系列的条件语句完成,结果是抛弃很多不期望的设备。

    有些PCI外围包含通用目的的PCI接口芯片和设备特定的电路。所有使用同样接口芯片的外围板子都有同样的签名,驱动程序必须进行额外的探测以保证它在处理正确的外围设备。因此,有时象jail_init_dev之类的函数必须准备好做一些设备特定的额外的检测,以抛弃那些可能有正确签名的设备。

     

    访问配置空间

    在驱动程序检测到设备后,它通常要对三个地址空间读或写:内存、端口和配置。特别地,访问配置空间对驱动程序来说极为重要,以呢这是它发现设备被映射到内存和I/O空间什么地方的唯一的办法。

    由于微处理器无法直接访问配置空间,计算机销售商必须提供一个办法来完成它。准确的实现因此是销售商相关的,与我们这里的讨论无关。幸运的是,这个事务的软件接口(下面描述)是标准化的,驱动程序或Linux核心都不需要知道它的细节。

    至于驱动程序,配置空间可以通过8位、16位、32位的数据传送来访问。相关函数的原型在<linux/bios32.h>

    int pcibios_read_config_byte(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

    int pcibios_read_config_word(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

    int pcibios_read_config_dword(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

        从由busfunction确定的设备的配置空间读取124个字节。参数where是从配置空间开始处的字节偏移。 从配置空间取出的值通过ptr返回,这些函数的返回值是错误代码。字和双字函数将刚从小印地安字节序读出的值转换为处理器本身的字节序,因此你并不需要处理字节序。

    int pcibios_write_config_byte(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned char val);

    int pcibios_write_config_word(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned short val);

    int pcibios_write_config_dword(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned int val);

        向配置空间里写124个字节。设备仍由busfunction确定,要写的值由val传递。字和双字函数在向外围设备写之前将数值转换为小印地安字节序。

    访问配置变量的最好办法是使用在<linux/pci.h>中定义的符号名。例如,下面的两行程序通过给pcibios_read_config_bytewhere传递符号名来获取一个设备的修正ID

    Unsigned char jail_get_revision(unsigned char bus, unsigned char fn)

    {

       unsigned char *revision;

     

           pcibios_read_config_byte(bus,fn, PCI_REVISION_ID,&revision);

           return revision;

    }

    当访问多字节值时,程序远一定要记住字节序的问题。

     

    看看一个配置快照

    如果你向浏览你系统上PCI设备的配置空间,你可以编译并加载模块pci/pcidata.c,它在O’Reilly FTP站点上提供的源文件中。

    这个模块生成一个动态的/proc/pcidata文件,包含有你的PCI设备配置空间的二进制快照。这个快照在文件每次被读时更新。/proc/pcidata的大小被限制为PAGE_SIZE字节(这是动态/proc文件的限制,在第四章“调试技术”中“使用/proc文件系统”一节介绍过)。这样,它只列出前PAGESIZE/256个设备的配置内存,意味着1632个设备(也许对你的系统已经够了)。我选择把/proc/pcidata作成二进制文件,而不是象其它/proc文件那样是文本的,就是因为这个大小限制。

    pcidata的另一个限制是它只扫描系统的第一条PCI总线。如果你的系统有到其它PCI总线的桥,pcidata将忽略它们。

    /proc/pcidata中设备出现的顺序与/proc/pci中相反。这是因为/proc/pci读的是一个从头部生长的链表,而/proc/pcidata则是一个简单的查找循环,它按照取到的顺序将所有的东西输出。

    例如,我的抓图器在/proc/pcidata的第二个出现,(目前)有下面的配置寄存器:

    morgana% dd bs=256 skip=1 count=1 if=/proc/pcidata | od –Ax –t x1

    1+0 records in

    1+0 records out

    000000 86 80 23 12 06 00 00 02 00 00 00 04 00 20 00 00

    000010 00 00 00 f1 00 00 00 00 00 00 00 00 00 00 00 00

    000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

    000030 00 00 00 00 00 00 00 00 00 00 00 00 0a 01 00 00

    000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

    如果你将上面的输出和图15-1比较,你就可以理解这些数字。或者,你可以使用pcidump程序,在可以从FTP站点上找到,它将输出列表格式化并标号。

    pcidump的代码并不值得在这儿列出,因为这个简单程序只是一个长表,外加十行扫描这个表的代码。相反,让我们看看一些选择的输出行:

    (代码351

    pcidatapcidump,与grep配合使用,对调试驱动程序的初始化代码非常有用。不过注意,pcidata.c模块是GPL的,因为我是从核心源码中取的PCI扫描循环。这不应该对你作为一个驱动程序的作者有什么影响,因为我只是以一个支持工具的形式将这个模块包含在源文件中,而不是新驱动程序的可重用模版。

     

    访问I/O和内存空间

    一个PCI外围实现六个地址区段。每个区段由内存或I/O位置组成,或者压根不存在。大多数设备用一个内存区段代替它们的I/O端口,因为有些处理器(象Alpha)没有本身的I/O空间,还因为PC上的I/O空间都相当拥挤。内存和I/O空间的结构化的不同通过实现一个“内存可预取”位*来表达。将其控制寄存器映射到内存地址范围的外围将这个范围声明为不可预取的,而PCI板子上的有些东西如视频内存是可预取的。在本节中,只要讨论适用于内存或I/O,我就用单词“区段”来指一个PCI地址范围。

    一个接口板子用配置寄存器(在图15-1中所示的632位寄存器,它们的符号名从PCI_BASE_ADDRESS_0PCI_BASE_ADDRESS_5)报告它的区段的大小和当前位置。由于PCI定义的I/O空间是一个32位的地址空间,因此用对内存和I/O适用同样的配置接口是可行的。如果设备使用64位的地址总线,它可以为每个区段用两个连续的PCI_BASE_ADDRESS寄存器在64位的内存空间来声明区段。因此有可能一个设备同时提供32位和64位的区段。

    我不想在这儿讨论太多的细节,因为如果你打算写一个PCI驱动程序,你总会这个设备的硬件手册的。特别地,我不打算使用寄存器的预取位或两个“类型”位,并且我将讨论限制在32位外围上。不过,了解一下一般情况下是如何实现的,以及Linux驱动程序是如何处理PCI内存是很有趣的。

    PCI规范要求每个被实现的区段百升微被映射到一个可配置地址上。这意味着设备必须位它实现的每个区段装备一个可编程32位解码器,并且利用64PCI扩展的板子必须有一个4位可编程解码器。尽管在PC上没有64PCI总线,一些Alpha工作站则有。

    由于通常一个区段的字节数是2的幂,如32644KB2MB,所以可编程解码器的实际实现和使用都被简化了。而且,将一个区段映射到一个未对齐的地址上意义也不大;1MB的区段自然在1M整数倍的地址处对齐,32字节的区段则在32的整数倍处。PCI规范利用了这个对齐;它要求地址解码器需要且只需查看地址总线的高位,并且只有高位是可编程的。这个约定也意味着任何区段的大小都必须是2 的幂。

    这样,重映射一个PCI区段可以通过在配置寄存器的高位设置一个合适的值来完成。例如,一个1M的区段,有20位的地址空间,可以通过设置寄存器的高12位进行重映射;向寄存器写0x008xxxxx告诉板子对8MB-9MB的地址区间响应。实际上,只有非常高的地址被用来映射PCI区段。

    这种“部分解码”有几个额外的好处就是软件可以通过检查配置寄存器中非可编程位的数目来确定PCI区段的大小。为了这个目的,PCI标准要求未使用的位必须总是读作0。通过强制I/O区段的最小大小为8字节,内存区段为16字节,标准可以把一些额外的信息放入同一个PCI寄存器中:“空间”位,表明区段是内存的还是I/O的;两个“类型”位;一个“预取”位,只是位内存定义的。类型位在32位区段、64位区段、以及“必须映射在1M一下的32位区段”进行选择。最后这个值用于那些仍然运行于一些PC上的过时软件。

    检测一个PCI区段的大小可以通过使用几个定义在<linux/pci.h>中的位掩码来简化:是个内存区段时PCI_BASE_ADDRESS_SPACE被置位;PCI_BASE_ADDRESS_MEM_MASK为内存区段掩去配置位;PCI_BASE_ADDRESS_TO_MASKI/O区段掩去这些位。规范还要求地址区段必须按序分配,从PCI_BASE_ADDRESS_0PCI_BASE_ADDRESS_5;这样一旦一个基地址未用(也就是被置未0),你就可以知道所有的后续地址都未用。

    报告PCI区段当前位置和大小的典型代码如下:

    (代码353

    (代码354 #1

    这个代码是pciregion模块的一部分,与pcidata在同一个目录下发布;这个模块生成一个/pci/pciregions文件,用上面给出的代码产生数据。当配置寄存器被修改时,中断报告被关闭,以防止驱动程序访问被映射到错误位置的区段。使用cli而不是save_flags是因为这个函数只在read系统调用时被执行,我们知道在系统调用的时候中断是打开的。

    例如,这里是我的抓图器的/proc/pciregion的报告:

    (代码 #2

    计算机的固件在引导时用一个类似于前面给出的循环来正确地映射区段。由于固件防止了任何地址赋值时的冲突,Linux驱动程序通常并不改变PCI区间的映射。

    有趣的是注意到上面的程序报告的内存大小有可能被夸大。例如,/proc/pciregion报告说我的视频板子是一个16MB的设备。但这并不真实(尽管我有可能扩展我的视频RAM)。但由于这个大小信息只是被固件用来分配地址区间,夸大区段大小对驱动程序的作者来说并不是一个问题,他设备的内部并能正确地处理由固件分配的地址区间。

     

    PCI中断

    至于中断,PCI很容易处理。计算机的固件已经给设备分配了一个唯一的中断号,驱动程序只需要去用它即可。中断号存在配置寄存器60中(PCI_INTERRUPT_LINE),它是一个字节宽。这允许最多256条中断线,但实际限制依赖于使用的CPU。驱动程序不必麻烦去检查中断号,因为在PCI_INTERRUPT_LINE中找到的一定是正确的。

    如果设备不支持中断,寄存器61PCI_INTERRUPT_PIN)为0;不然为非0。不过由于驱动程序知道它的设备是否是中断驱动的,因此并不常需要去读PCI_INTERRUPT_PIN

    这样,处理中断的PCI特定的代码只需要这个配置字节以取得中断号,如下面所示的代码。不然,应用第九章的信息。

          result = pcibios_read_config_byte(bus,fnct,PCI_INTERRUPT_LINE, &my_irq);

          if(result){/*deal with result*/}

    本节的其余部分为感兴趣的读者提供一些额外的信息,但对写驱动程序并不需要。

    一个PCI连接器有四个中断脚,外围板子可以任意使用。每个管脚都是独立地路由到主板的中断控制器,因此中断可以共享,而没有任何电气问题。中断控制器负责将中断线(脚)映射到处理器的硬件;将这个平台相关的操作留给控制器是为了获得总线本身的平台无关性。

    位于PCI_INTERRUPUT_PIN的只读配置寄存器用来告诉计算机哪一个管脚被使用了。值得记住的是每个设备板子最多可带8个设备;每个设备使用一个中断脚并在它自己的配置寄存器中报告它。同一个设备板子的不同设备可以使用不同的中断脚,也可以共享同一个。

    另一方面,PCI_INTERRUPT_LINE寄存器是读/写的。在计算机引导时,固件扫描它的PCI设备,并按照中断脚是如何路由到它的PCI槽的为每个设备设置这个寄存器。这个值由固件来赋,因为只有固件知道母板是如何将不同的中断脚路由到处理器的。然而,对设备驱动程序来说,PCI_INTERRUPT_LINE寄存器是只读的。

     

    回顾:ISA

     

    ISA总线在设计上相当老了,而且在性能方面也名声扫地,但它依然占据着扩展设备的很大一块市场。如果速度不是很重要,并且你想支持旧的主板,那么ISA实现要比PCI更令人喜欢。这个旧标准的一个额外的优势是,如果你是个电子爱好者,你可以很容易地构造你自己的设备。

    令一方面,ISA的一个巨大的缺点是它紧密地绑定在PC体系结构上;接口总线具有80286处理器的所有限制,导致系统程序员无穷的痛苦。ISA设计的令一个巨大的问题(从原先的IBM PC继承下来的)是缺乏地理寻址,这导致了无穷的问题和为加一个新设备时漫长的“拔下--重跳线插上测试”循环。有趣的是注意到即使是最老的Apple II计算机都已经利用了地理寻址,它们的特征是无跳线的扩展板。

     

    硬件资源

    一个ISA设备可以装配I/O端口,内存区域,和中断线。

    即使x86处理器支持64KBI/O端口内存(也就是说,处理器申明16根地址线),有些老的PC硬件也只能对最低的10根地址线解码。这将可用的地址空间限制为1024个端口,因为在1KB-64KB区间的任何地址会被任何只能解码低地址线的设备错误地看成低地址。一些外围通过只映射一个端口到低KB,并使用高地址线在不同的设备寄存器中选择的办法绕过了这个限制。例如,一个映射到0x340的设备可以安全地使用端口0x7400xB40,等等。

    如果说I/O端口的可用性受到了限制,那么内存访问就更糟了。一个ISA设备只能使用640KB-1MB15MB-16MB之间的内存区间。640KB-1MB区间被PC BIOSVGA兼容的视频板、以及各种其它设备使用,留给新设备很少的可用空间。另一方面,15M处的内存,Linux并不直接支持;这个问题在第八章的“访问设备板子上的内存”讨论过。

    ISA设备板子上第三个可用的资源是中断线。有限的中断线被路由到ISA总线,它们被所有的接口板共享。造成的结果是,如果设备没有被正确地配置,它们可以用同样的中断线找到它们自己。

    尽管原先的ISA规范不允许跨设备的中断共享,多数设备板子还是允许的*。软件级的中断共享在第九章的“中断共享”中描述过。

     

    ISA程序设计

    至于程序设计,除了Linux核心通过维护I/OIRQ寄存器提供有限的帮助外(在第二章中的“使用资源”和第九章的“安装中断处理程序”中描述过),核心和BIOS中没有任何东西使得使用ISA设备更容易一些。

    本书整个第一部分给出的编程技巧同样适用于ISA设备;驱动程序可以探测I/O端口,中断线必须用在第九章“自动检测IRQ号”介绍过的技术之一来自动检测。

     

    “即插即用”规范

    有些ISA设备板子遵循特殊的设计准则,要求特别的初始化序列,以简化增加接口板的安装和配置。这类板子的设计规范被称做“即插即用(PnP)”,它由一组构造和配置无跳线ISA设备的繁杂的规则集组成。PnP设备实现了可重定位的I/O区段;PCBIOS负责这个重定位—--PCI的风格。

    简单地说,PnP的目的就是PCI设备具有的同样的灵活性,而不改变底层的电气接口(ISA总线)。为了这个目的,规范定义了一组设备无关的配置寄存器和地理寻址接口板的方法,即使物理总线并不携带每个板子(地理的)的走线----每个ISA信号线于每个可用槽相连。

    地理寻址工作的方式是:给计算机的每个ISA外围分配一个小整数,称做“卡选择号(CSN)”。每个PnP设备有一个唯一的序列标志符,64位宽,被硬写入外围板子。CSN的分配用这个唯一的序列号来确定PnP设备。但CSN只能在引导时被安全地分配,这要求BIOS理解PnP。由于这个原因,如果没有一个配置盘,老计算机不能支持PnP

    符合PnP规范的接口板在硬件级十分复杂。它们比PCI板要精细的多,同时要求符杂的软件。安装这类设备遇到困难并不罕见;即使安装没有问题,你仍然面对性能限制和ISA总线有限的I/O空间。按我的观点,只要可能,最好是安装PCI设备并享受其新技术。

    如果你对PnP的配置软件有兴趣,你可以浏览drivers/net/3c509.c,它的探测函数处理了PnP设备。Linux2.1.33也为PnP增加了一些初始支持,见目录drivers/pnp

     

    其它PC总线

    PCIISAPC世界最常用的外围接口,但它们并不为仅有。下面是PC市场上找得到的其它总线特征的概述。

     

    MCA

    “微通道体系结构(MCA)”是用在PS/2计算机和一些笔记本上的一个IBM标准。微通道的主要问题是缺乏文档,这导致了Linux上对MCA支持的缺乏。不过,在2.1.15,已经飘荡多时的MCA补丁被加入了正式的核心;因此,新的核心可以在PS/2计算机上运行。

    在硬件级,微通道具有比ISA多的特征。它支持多主DMA32位地址和数据线,共享中断线,以及访问每个板子配置寄存器的地理寻址。这种寄存器被称做“可编程选项选择(POS)”,但它们并不具有PCI寄存器的所有特征。LinuxMCA的支持包括一些引出到模块的函数。

    设备驱动程序可以通过读取整数值MCA_bus来确定它是否运行在一个微通道计算机上。如果核心运行在一个MCA单元中,那么MCA_bus0。如果这个符号是个预处理器宏,那么宏MCA_bus__is_a_macro也要被定义。如果MCA_bus__is_a_macro未定义,那么MCA_bus是引出到模块化代码的一个整数变量。事实上,MCA_bus对除PC外的所有平台仍然是个硬写为0的宏----Linux X86的移植在2.1.15将宏改为变量。MCA_busMCA_bus__is_a_macro都在<asm/processor.h>中定义。

     

    EISA

    扩展的ISAEISA)是ISA32位扩展,带一个兼容的接口连接器;ISA的设备板子可以插入一个EISA连接器。额外的线路是在ISA连接下路由。

    PCIMCAEISA总线被设计来带无跳线设备,它与MCA有同样的特征:32位地址和数据线,多主DMA,以及共享中断线。EISA设备由软件配置,但它们不需要任何特别的操作系统支持。EISA驱动程序已经为以太网设备和SCSI控制器存在与Linux核心中。

    EISA驱动程序检查EISA_bus确定是否主机带有EISA总线。类似于MCA_busEISA_bus要么是宏,要么是变量,这依赖于EISA_bus__is_a_macro是否被定义了。这两个符号定义在<asm/processor.h>

    至于驱动程序,核心中没有对EISA的特别支持,程序员必须自己处理ISA的扩展。驱动程序用标准的EISA I/O操作访问EISA寄存器。核心中已有的驱动程序可以作为示例代码。

     

    VLB

    ISA的另一个扩展是“VESA Local Bus”接口总线,它通过增加一个长度方向的槽扩展ISA连接器。这个额外的槽可以被VLB设备“单独”使用;由于它从ISA连接器复制了所有重要的信号,设备可以被构造成只插入VLB插槽,而不用ISA插槽。单独的VLB外围很少见,因为多数设备需要到达背板,这样它们的外部连接器是可用的。

     

    Sbus

    虽然多数Alpha计算机装备有PCIISA接口总线,多数基于Sparc的工作站使用Sbus连接它们的外围。

    Sbus是相当先进的一种设计,尽管它已经存在了很长时间了。它是想成为处理器无关的,并专为I/O外围板子进行了优化。换句话说,你不能在Sbus的槽中插入额外的RAM。这个优化的目的是简化硬件设备和系统软件的设计,这是以母板上的额外复杂性为代价的。

    总线的这种I/O偏向导致了一类外围,它们用虚地址传送数据,这样绕过了分配连续缓冲区的需要。母板负责解码虚地址,并映射到物理地址上。这要求在Sbus上附带一些MMU(内存管理单元)的能力,这部分负责的电路被称为“IOMMU”。这种总线的另一个特征是:设备板子是地理寻址的,因此不需要在每个外围上实现地址解码器,也不需要处理地址冲突。

    Sbus外围在PROM中使用Forth语言来初始化它们。选择Forth是因为这个解释器是轻量级的,可以容易地在任何计算机系统的固件里实现。另外,Sbus规范描述了引导过程,因此符合条件的I/O设备可以简单地接入系统,并在系统引导时被识别出来。

    至于Linux,直到核心2.0也没有对Sbus设备的特别支持被引出到模块中。版本2.1.8增加了对Sbus的特定支持,我鼓励感兴趣的读者去看看最近的核心。

     

    快速参考

    本节象往常一样,概述在本章中介绍的符号。

    #include <linux/config.h>

    CONFIG_PCI

    这个宏用来条件编译PCI相关的代码。当一个PCI模块被加载入一个非PCI核心时,insmod会抱怨说几个符号不能解析。

    #include <linux/pci.h>

    这个头文件包含PCI寄存器和几个销售商及设备ID的符号名。

    #include <linux/bios32.h>

    所有下面列出的pcibios_函数在这个头文件中定义原型。

    int pcibios_present(void);

    这个函数返回一个布尔值表明我们运行的计算机是否具有PCI能力。

    int pcibios_find_device(unsigned short vendor, unsigned short id, unsigned short index,

                       unsigned char *bus, unsigned char *function);

    int pcibios_find_class(unsigned int class_code, unsigned short index,

                      unsigned char *bus, unsigned char *function);

    这些函数询问PCI固件关于设备是否有某个特定的签名,或属于某个特定的类。返回值是一个出错说明;成功时,busfunction用来存储设备的位置。index第一次必须被传给0,以后没查找一个新设备,就增加1

    PCIBIOS_SUCCESSFUL

    PCIBIOS_DEVICE_NOT_FOUND

    char *pcibios_strerror(int error);

    这些宏,还有其它几个表示pcibios函数返回的整数值。DEVICE_NOT_FOUND一般被认为是个成功值,因为成功地发现没有设备。pcibios_strerror函数用来将每个整数返回值转换为一个字符串。

    int pcibios_read_config_byte(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

    int pcibios_read_config_word(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

    int pcibios_read_config_dword(unsigned char bus, unsigned char function,

                            unsigned char where, unsigned char *ptr);

    int pcibios_write_config_byte(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned char val);

    int pcibios_write_config_word(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned short val);

    int pcibios_write_config_dword(unsigned char bus, unsigned char function,

                             unsigned char where, unsigned int val);

    这些函数用来读写PCI配置寄存器。尽管Linux核心负责字节序,程序员在从单个字节组装多字节值时必须特别注意字节序。PCI总线是小印地安字节序。



    * 你可以在它自己的硬件手册中找到任何设备的ID

    * 这个信息居于基地址PCI寄存器的某个低序位中。

    * 中断共享的问题是属于电气工程的;如果一个设备驱动程序驱动信号线为不活动----通过应用一个低阻抗电压级----中断便不能共享。另一方面,如果设备对不活动逻辑级使用一个上拉电阻,那么共享就是可能的。多数ISA接口板使用上拉的方法。

     
    展开全文
  •  1)、键盘的每种类型都有特定的输入设备驱动程序,但所有的都使用相同的键盘事件处理程序,以确保提供欸用户的接口一致。和其他的事件驱动程序相比,键盘事件驱动  程序有其独特之处:它传送数据给另一个内...
  • 从驱动加载方式来区分 在深入探讨Windows CE所支持的外围设备驱动程序之前,先了解在WinCE平台上使用的两种设备:内建设备和可安装设备。因此,从驱动加载方式来看WinCE可分为本机设备驱动(Built-In Driver)、可...
  • PCI(外围设备互联)接口   PCI总线是当今普遍使用在桌面以及更大型计算机上的外设总线,而且该总线是内核中得到最好支持的总线。尽管许多计算机用户将PCI看成是一种布置电子线路的方式,但实际上它是一组完整的...
  • Linux下PCI设备驱动程序开发

    千次阅读 2014-02-26 11:13:44
    Linux下PCI设备驱动程序开发 PCI是一种广泛采用的总线标准,它提供了许多优于其它总线标准(如EISA)的新特性,目前已经成为计算机系统中应用最为广泛,并且最为通用的总线标准。Linux的内核能较好地支持PCI总线...
  • DMA(Linux设备驱动程序

    千次阅读 2016-04-22 21:03:51
    DMA需要设备驱动程序分配一个或者多个适合执行DMA的特殊缓冲区。 许多驱动程序在初始化阶段分配了缓冲区,一直使用它们直到关闭。 分配DMA缓冲区 设备使用ISA或者PCI系统总线传输数据时,这两种方式使用的都...
  • 开发板:mini2440 内核版本:linux2.6.32.2 内容概括:  1、adapter client 简介  2、adapter 驱动框架  2.1 设备侧  2.2 驱动侧  2.2.1 probe 函数  2.2.1.1 注册adapter  new_device de
  • linux内核Device Drivers设备驱动程序

    千次阅读 2015-10-14 11:25:37
    设备驱动程序 Generic Driver Options 驱动程序通用选项  Select only driversthat don't need compile-time external firmware 只显示那些不需要内核对外部设备的固件作map支持的驱动程序,除非你有某些怪异...
  • Linux下PCI设备驱动程序详解

    千次阅读 2014-08-26 16:18:15
    PCI 是一种广泛采用的总线标准,它提供了许多优于其它总线标准... 386体系结构为主,探讨了在Linux下开发PCI设备驱动程序的基本框架。 一、PCI总线系统体系结构 PCI是外围设备互连(Peripheral Component
  • 问题现象:当使用手机的蓝牙和电脑连接时,在Windows7下的设备管理器可能会出现两个或三个bluetooth 外围设备驱动有问题的这主要是没有驱动而产生的,这和蓝牙的驱动无关,有也只能找Nokia(我的手机是Nokia N70)...
  • USB HID设备驱动程序设计

    千次阅读 2014-12-19 11:12:50
    它的出现大大简化了PC机和外围设备的连接过程,使PC机接口的扩展变得更加容易。USB作为近年来计算机和嵌入式领域中的热点,推动了计算机外设的 飞速发展。本文介绍了适用于PC的嵌入式操作系统的USB HID设备驱动的...
  • 1. 设备驱动程序的概念 编写驱动程序之前,你要有一台运行UNIX的机器。你一定要熟悉一般的驱动概念,以及成功编写的驱动程序,您需要执行特定的任务。 这本书假定您了解以下概念: a.设备驱动程序的目的 b.设备...
  • 我们已经讨论了字符设备和块设备驱动程序,接着要讨论的是迷人的网络世界。网络接口是Linux设备中的第三标准类,这一章就是讲述它们是如何与核心的其余部分交互的。网络接口并不象字符和块设备那样存在于文件系统。...
  • 今天装打印机驱动时偶尔发现设备管理器中的未知设备栏有BlueTooth外围设备,然后在百度上一顿搜索,发现解决方法全是靠删除bluetooth设备或者蓝牙上网服务来实现的。这只是个治标不治本的烂方法。不爽的我在一个叫

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 22,513
精华内容 9,005
关键字:

外围设备驱动程序如何安装