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( )通常需要完成下列工作:
  
  1.检查设备相关错误,如设备尚未准备好等。
  
  2.如果是第一次打开,则初始化硬件设备。
  
  3.识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。
  
  4.分配和填写要放在file->private_data里的数据结构。
  
  5.使用计数增1。
  
  释放设备是通过调用file_operations结构中的函数release( )来完成的,这个设备方法有时也被称为close( ),它的作用正好与open( )相反,通常要完成下列工作:
  
  1.使用计数减1。
  
  2.释放在file->private_data中分配的内存。
  
  3.如果使用计算为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 memory/n");
      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。

  第二章 PCI总线简介
  21 Linux PCI总线的支持
  PC机发展至今,出现了许多的总线标准:PCI、 ISAMCAEISAVLBSbus等都是当今PC市场上能找得到的总线标准。其中PCIISAPC世界最常用的外设接口,但由于ISA在设计上已经相当陈旧了,现在有PCI将全面替代ISA的趋势,因此,PCI总线已成为当今最常用的外设总线,也是 Linux内核 支持最好的总线。
  22 PCI总线概览
  PCI是一组完全的规范,它定义了计算机的不同部分是如何交互的。PCI规范覆盖了与计算机接口相关的绝大多数方面,本文主要讨论一个PCI驱动程序是如何找到它的硬件,并获得对它的访问的。
  PCI外设由一个总线号、一个设备号、和一个功能号确定。
  PCI设备有三种地址空间,分别是:内存空间、I/O共享空间和 配置 空间。前两个地址空间由PCI总线上的所有设备共享,而配置空间则是设备私有的。每个PCI插槽有一个配置事务的私用使能线,PCI控制器一次只能访问一个外设,不会有 地址冲突 。所有这些空间对于 CPU 来说都是可以访问的。在初始化阶段,外设的配置信息被Linux内核中的初始化程序从配置空间读出。这样,一旦配置寄存器被读出,不需通过探测,驱动程序就可以访问它的硬件了。
  23 PCI配置寄存器
  23配置寄存器布局
  PCI配置寄存器的布局是标准化的,由256字节的地址空间组成,其中前64个字节是标准化的,其余的则与具体设备相关。
  下表显示了与设备无关配置空间的布局 。
  配置地址偏移 寄存器英文名 寄存器中文名
  00H01H Vendor ID 制造商标识
  02H03H Device ID 设备标识
  04H05H Command 命令寄存器
  06H07H Status 状态寄存器
  08H Revision ID 版本识别号寄存器
  09H0bH Class Code 分类代码寄存器
  0cH Cache Line Size CACHE行长度寄存器
  0dH Latency Timer 主设备延迟时间寄存器
  0eH Header Type 头标类型寄存器
  0fH Bulit-in-teset Register 内含自测试寄存器
  10H13H Base Address Register 0 基地址寄存器0
  14H17H Base Address Register 1 基地址寄存器1
  18H1bH Base Address Register 2 基地址寄存器2
  1cH19H Base Address Register 3 基地址寄存器3
  20H23H Base Address Register 4 基地址寄存器4
  24H27H Base Address Register 5 基地址寄存器5
  28H2bH Cardbus CIS Pointer 设备总线CIS指针寄存器
  2cH2dH Subsystem Vendor ID 子设备制造商标识
  2eH2fH Subsystem Device ID 子设备标识
  30H33H Expasion ROM Base Address 扩展ROM基地址
  34H3bH ——— 保留
  3cH Interrupt Line 中断线寄存器
  3dH Interrupt Pin 中断引脚寄存器
  3eH Min_Gnt 最小授权寄存器
  3fH Max_Lat 最大延迟寄存器
  讨论所有的配置项显然超出了本文的讨论范围,通常,与设备一起发布的技术资料会详细描述它支持的寄存器。我们只对那些有助于驱动程序找到设备的配置项感兴趣,它们是:VendorIDDeviceIDClassCode。每个PCI外设都会把自己的值放入这些只读寄存器,驱动程序可以用它们来查找设备。
  下面的头文件,宏,以及函数都将被PCI驱动程序用来寻找它的硬件设备:
  #include 
   驱动程序需要知道PCI函数在核心是否是可用的。通过包含这个头文件,驱动程序获得了对CONFIG_宏的访问,包括CONFIG_PCI
  CONFIG_PCI
  如果核心支持对PCI BIOS的调用,那么这个宏被定义。并不是每台计算机都有PCI总线,所以核心的开发者应该把 PCI的支持做成编译时的可选项,从而在无PCI的计算机上运行Linux时节省内存。如果CONFIG_PCI没有定义,那么这个列表中其它的函数都不可用,驱动程序应使用预编译的条件语句将针对PCI的语句全都排除在外。
  #include 
  这个头文件声明了以下介绍的函数。这个头文件还定义了函数返回的 错误代码 的符号值。
  int pcibios_present(void)
  由于与PCI相关的函数在无PCI总线的计算机上是毫无意义的,pcibios_present函数就是告诉驱动程序计算机是否支持PCI;如果支持,它返回一个为真布尔值。在调用下面介绍的函数之前最好先检查一下pcibios_present,以保证计算机支持PCI
  #include 
  这个头文件定义了下面函数使用的所有数值的符号名。并不是所有的设备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请求相关设备的信息。vendor / id对用来确定设备。index用来支持具有同样的vendor / id对的几个设备。对这个函数的调用返回设备在总线上的位置以及函数指针。返回代码为0表示成功,非0表示失败。
  int pcibios_find_class(unsigned int class_code,
  unsigned short index,
  unsigned char *bus,
  unsigned char *function);
  这个函数和上一个类似,但它寻找属于特定类的设备。返回代码为0表示成功,非0表示有错。
  char *pcibios_strerror(int error)
  这个函数用来将一个PCI错误代码翻译为字符串。
  23访问配置寄存器
  当驱动程序检测到设备后,就可以对内存、I/O和配置空间进行读或写了。特别地,访问配置空间对于驱动程序来说极为重要,因为这是它发现设备被映射到内存和I/O空间某地方的唯一方法。
  驱动程序或Linux内核可以使用以下的软件接口,以8位、16位、32位的数据来访问配置空间。这些函数都是标准化的,相关原型在中:
  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 char val);
  int pcibios_ write_config_dword( unsigned char bus,
  unsigned char function,
  unsigned char where,
  unsigned char val);
  它们分别向配置空间里写124个字节。设备仍由busfunction确定,要写的值由val传递。

 2.4 访问I/O和内存空间
   配置 项PCI_BASE_ADDRESS_0 到PCI_BASE_ADDRESS_5表示PCI外设的六个地址区段(这里的“区段”指一个PCI地址范围),每个区段可以由内存或I/O位置组成,或者根本不存在。 由于PC上的I/O空间已经相当拥挤,且有的处理器(如Alpha)自身没有I/O空间,因此大多数设备用一个内存区段代替它们的I/O 端口。
  PCI定义的I/O空间是一个32位的地址空间,如果设备使用64位的地址总线,那么它可以为每个区段用两个连续的PCI_BASE_ADDRESS寄存器在64位的内存空间来声明区段。
  然后我们就可以用前面讲到的函数来读写区段地址了, 例如:
  pcibios_read_config_dword(bus,fun, PCI_BASE_ADDRESS_0,&port);
  将PCI_BASE_ADDRESS_0代表的地址读到port中。
  pcibios_write_config_dword(bus,fun, PCI_BASE_ADDRESS_3,val);
  将val的值写到PCI_BASE_ADDRESS_3中。
  如何访问I/O端口呢?
  在已得到了I/O端口地址的前提下,可以使用 Linux内核 头文件中定义的函数访问I/O端口:
  unsigned char inb(unsigned short port);
  void outb(unsigned char byte,unsigned short port);
  按字节读写8位端口。
  unsigned short inw(unsigned short port);
  void outw(unsigned short word,unsigned short port);
  按字宽度读写16位端口。
  unsigned long inl(unsigned short port);
  void outl(unsigned long word,unsigned short port);
  按双字读写32位端口。
  注意,port参数在x86平台上定义为unsigned short,但在Alpha平台上定义为unsigned long。
  第三章 可加载的内核模块:
  3.1 Linux内核与驱动程序
  在 Linux系统 中,若干并发 进程 执行着不同的任务。每个进程都可能有获得系统资源的要求。内核是一整块可执行代码,它负责处理所有这样的请求。内核可以被划分为以下这些部分: 进程管理 、内存管理、文件系统、设备控制、网络。
  Linux有一个很好的特性,即通过加载模块可以扩展内核代码,也就是说可以随时增加系统的功能。而本文所要讨论的PCI设备驱动程序其实也是加载到内核中的一个模块。
  世界各地钻研Linux内核的人群当中,大多是在写设备驱动程序。尽管每个驱动程序都不一样,而且还需知道自己设备的特殊细节,但是这些设备驱动程序的许多原则和基本技术技巧都是一样的。
  在编写驱动程序时,程序员应该注意以下问题:不同用户有不同的需求,程序员编写内核代码访问硬件时,不能强迫用户采用某种特定的策略。设备驱动程序应该仅仅处理硬件,将如何使用硬件的问题留给应用程序。我们可以这样来看待我们所编写的驱动程序:它是位于应用层与实际设备之间的软件。程序员可以使不同的驱动程序提供不同的能力,甚至相同的设备也可以提供不同的能力,只要使用不同的驱动程序即可。
  3.2 模块与应用程序
  3.2.1 内核模块与应用程序之间的区别
  一个应用程序从头到尾完成一个任务;而模块是可以在系统启动之后任何时刻动态连接到核心的代码块,它们可以在系统不再需要它们时从核心删除并卸载。 init_module( )(模块的入口点)在加载模块时被调用,其任务就是为以后调用模块的函数做准备;模块的第二个入口点,cleanup_module,仅当模块被下载前才被调用。能够卸载是模块化的优良特性之一,这可以使 程序开发 者减少开发时间:无需每次都花很长的时间开关机就可以测试所编写的驱动程序。
  内核编程和应用程序编程还有一个区别,就是它们出错后所造成的后果不同:在应用程序开发期间,段违例是无害的,利用调试器可以轻松地跟踪到引起问题的错误之处;然而内核失效却是致命的,即使不至于使整个系统崩溃,那至少会使当前进程无法继续运行。
  在涉及到内核模块与应用程序之间的区别时,还得注意一下“名字空间污染”问题:即存在很多函数和全局变量时,它们的名字已不再富有足够的意义来很容易地区分彼此的问题。在编写应用程序时,程序员就必须花大量的精力来记住某些“保留”名,并为新符号寻找新的唯一的名字。而在编写内核代码时如果出现“名字空间污染”问题,那对程序员来说简直是无法容忍的,因为即便是最小的模块也要连接到整个内核中。防止此类问题出现的方法是把所有自己定义的符号都声明为 static。此外,也可以通过声明一个符号表来避免对所有符号都使用static声明。
  3.2.2 用户空间和内核空间
   操作系统 要为程序提供一个计算机硬件一致的视图;同时,操作系统有处理程序的独立操作,并防止对资源的未经授权的访问。这就要求 CPU 具有可以防止 系统软件 免受 应用软件 干扰的保护机制,而每种现代处理器都能实现这种功能。实现的方案就是在CPU内部实现不同的操作模式(或级),不同的级有不同的权限,而且某些操作不允许在最低级使用,程序代码只能从一个级切换到另一个级。在Linux系统中,执行态分最高级(也称为“管理员态”)和最低级(也称为“用户态”),它们分别对应“内核空间”和“用户空间”。模块就是在“内核空间”运行的,而应用程序则是在“用户空间”中运行的。
  3.3 模块的基本结构
  内核模块至少必须包含两个函数:init_module和cleanup_module。第一个函数是在把模块加载入内核时调用的;第二个函数则是在删除该模块时调用。一般说来,init_module向内核注册模块所能提供的所有新功能,即可以由应用程序使用的新功能。函数 cleanup_module的任务是清除掉init_module所做的一切,这样,这个模块就被安全地卸载了。

 

 

你可能感兴趣的:(linux,驱动)