• 【VS开发】【DSP开发】浅谈Linux PCI设备驱动(一)


        要弄清楚Linux PCI设备驱动,首先要明白,所谓的Linux PCI设备驱动实际包括Linux PCI设备驱动和设备本身驱动两部分。不知道读者理不理解这句话,本人觉得这句话很重要,对于PCI、USB这样的驱动来说,必须要理解这个概念,才能明白该如何看待Linux下的PCI和USB以及类似的总线型的驱动。理由也很简单,就是Linux PCI驱动是内核自带的,或者说内核帮你写好了!而我们需要完成的就是设备本身的驱动,比如网卡驱动等。当然,并不是说内核帮咱们写好了Linux PCI驱动我们什么就不用做了,至少你要明白内核大致都干了些什么,这样你才能明白你该干什么,如何完成设备本身的驱动。这跟我们学习操作系统时要学习很多系统调用接口一样的道理,不知道这些接口,怎么用操作系统或者说操作系统给你提供的功能呢? 所以这里我们就来研究下Linux PCI驱动到底都干了些什么,以便我们在此基础上完成我们设备本身的驱动。

    http://tldp.org/LDP/tlk/dd/pci.html这篇文章里(整本书叫做The Linux Kernel,中文翻译见http://oss.org.cn/ossdocs/linux/kernel/ 本文也参考了该中文翻译)  提到了:

    Linux PCI 初始化代码逻辑上分为三个部分:
    (1)PCI设备驱动程序即上面提到的Linux PCI设备驱动
    这个伪设备驱动程序从总线0开始查询PCI系统并且定位系统中所有的PCI设备和PCI桥。它建立一个
    可以用来描述这个PCI系统拓朴层次的数据结构链表。并且对所有的发现的PCI桥编号。
    (2)PCI BIOS
    这个软件层提供在bib-pci-bios归约中描述的服务。虽然Alpha AXP不提供BIOS服务,在其Linux
    版本中包含了相应的功能。
    (3)PCI Fixup 
    与特定系统相关的PCI初始化修补代码

    而这里主要就是探讨Linux PCI设备驱动,会在最后列出一段包含设备本身驱动的示例代码,仅供参考。

    一、概述及简介

        PCI(Periheral Component Interconnect)有三种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间。其中,PCI I/O空间和PCI内存地址空间由设备驱动程序(即上面提到的设备本身驱动)使用,而PCI配置空间由Linux PCI初始化代码使用,这些代码用于配置PCI设备,比如中断号以及I/O或内存基地址。所以这里的PCI设备驱动就是要大致描述对于PCI设备驱动,Linux内核都帮我们做了什么(主),接着就是我们应该完成什么(次)。

    (1)Linux内核做了什么

        简单的说,Linux内核主要就做了对PCI设备的枚举和配置;这些工作都是在Linux内核初始化时完成的。

        枚举:对于PCI总线,有一个叫做PCI桥的设备用来将父总线与子总线连接。作为一种特殊的PCI设备,PCI桥主要包括以下三种:

        1). Host/PCI桥:  用于连接CPU与PCI根总线,第1个根总线的编号为0。在PC中,内存控制器也通常被集成到Host/PCI桥设备芯片中,因此Host/PCI桥通常也被称为“北桥芯片组(North Bridge Chipset)”。

        2). PCI/ISA桥:   用于连接旧的ISA总线。通常,PCI中类似i8359A中断控制器这样的设备也会被集成到PCI/ISA桥设备中。因此,PCI/ISA桥通常也被称为“南桥芯片组(South Bridge Chipset)”

        3). PCI-to-PCI桥(以下称为PCI-PCI桥):  用于连接PCI主总线(Primary Bus)和次总线(Secondary Bus)。PCI-PCI桥所处的PCI总线称为主总线,即次总线的父总线;PCI-PCI桥所连接的PCI总线称为次总线,即主总线的子总线。

        下图摘自PCI Local Bus Specification Revision 2.1,可以看到PCI-PCI桥的Class Code(见图3)就是0x060400。

        CPU通过Host/PCI桥与一条PCI总线相连,处在这种位置上的PCI总线称为根总线。PC机中通常只有一个Host/PCI桥,在一条PCI总线的基础上,可以再通过PCI桥连接到其他次一层的总线,例如通过PCI-PCI桥可以连接到另一条PCI总线,通过PCI-ISA桥可以连接到一条ISA总线。事实上,现代PC机中的ISA总线正是通过PCI-ISA桥连接在PCI总线上的。这样,通过使用PCI-PCI桥,就构筑起了一个层次的、树状的PCI系统结构。对于上层的总线而言,连接在这条总线上的PCI桥也是一个设备。但是这是一种特殊的设备,它既是上层总线上的一个设备,实际上又是上层总线的延伸。 所谓枚举,就是从Host/PCI桥开始进行探测和扫描,逐个“枚举”连接在第一条PCI总线上的所有设备并记录在案。如果其中的某个设备是PCI-PCI桥,则又进一步再探测和扫描连在这个桥上的次级PCI总线。就这样递归下去,直到穷尽系统中的所有PCI设备。其结果,是在内存中建立起一棵代表着这些PCI总线和设备的PCI树。每个PCI设备(包括PCI桥设备)都由一个pci_dev结构体来表示,而每条PCI总线则由pci_bus结构来表示。你有通过PCI桥建立起的硬件设备树,我有内存中通过数据结构构建的软件树,多么和谐 呵呵。

                                                                      图1    PCI系统示意图   

        配置:PCI设备中一般都带有一些RAM和ROM 空间,通常的控制/状态寄存器和数据寄存器也往往以RAM区间的形式出现,而这些区间的地址在设备内部一般都是从0开始编址的,那么当总线上挂接了多个设备时,对这些空间的访问就会产生冲突。所以,这些地址都要先映射到系统总线上,再进一步映射到内核的虚拟地址空间。而所谓的配置就是通过对PCI配置空间的寄存器进行操作从而完成地址的映射(只完成内部编址映射到总线地址的工作,而映射到内核的虚拟地址空间是由设备本身的驱动要做的工作)。

    (2)Linux内核怎么做的

        这里首先要说明的是,对于PCI的设备初始化(即上面提到的枚举和配置工作),PC机的BIOS和Linux内核都可以做。一般而言,只要是采用PCI总线的PC机,其BIOS就必须提供对PCI总线操作的支持,因而称为PCI BIOS。而且最早Linux内核也是通过这种BIOS调用的方式来获取系统中的PCI设备信息的,但是不是所有的平台都有BIOS(如某些嵌入式系统),并且在实践中也发现有些母板上的PCI BIOS存在这样那样的问题,所以后来就改由Linux内核自己动手了,自己动手 丰衣足食 呵呵。不过,Linux内核还是很体贴的在make menuconfig的选项里为我们提供了自己选择的权利,即PCI access mode,里面提供了四个选项分别是BIOS、MMconfig、Direct和Any。Direct方式就是抛开BIOS而由内核自己完成初始化工作的意思。

                                                                                                         

    二、开始我们的枚举与配置之路

         注:为了更清晰,简单的描述PCI设备的初始化过程(因为2.4.18中还没有引入设备驱动模型,这样可以让我们专心研究PCI设备驱动本身)。这里是对Linux-2.4.18的内核进行的分析,主要原因大家从参考资料中也应该能明白,这里很多就是参考了[1]中的资料来分析的。如果想学PCI设备驱动,那么应该好好看看[1]的第八章中的PCI总线一节。然后再能找到一个驱动的例子代码看看,就可以说算是对PCI设备驱动入门了,当然,前提是都看懂了 呵呵。

        废话少说,下面进入正题。前面提到了PCI有三种地址空间,其中的PCI配置空间是给Linux内核中的PCI初始化代码用的,也就是我们这里的枚举与配置时用到的。那么这个PCI配置空间里放的是什么东西呢,显然应该是寄存器,称为配置寄存器组。当PCI设备上电时,硬件保持未激活状态。即该设备只会对配置事务做出响应。上电时,设备上不会有内存和I/O端口映射到计算机的地址空间;其他设备相关的功能,例如中断报告,也被禁止。

        PCI标准规定每个设备的配置寄存器组最多可以有256字节的连续空间,其中开头的64字节的用途和格式是标准的,称为配置寄存器的头部。系统中提供一些与硬件有关的机制,使得PCI配置代码可以检测在一个给定的PCI总线上所有可能的PCI配置寄存器头部,从而知道哪个PCI插槽上目前有设备,哪个插槽上暂无设备。这是通过读PCI配置寄存器头部上的某个域完成的(一般是“Vendor ID" 域)。如果一个插槽上为空,上述操作会返回一些错误返回值,如0xFFFFFFFF。这种头部(指64字节头部)又有三种,其中“0型”(type 0)头部用于一般的PCI设备,“1型”头部用于各种PCI-PCI桥, “2型”头部是用于PCI-CardBus桥的,CardBus是笔记本电脑中使用的总线,我们不关心。而64字节头部中的16个字节中又包含着有关头部的类型、设备的种类、设备的一些性质、由谁制造等等信息。根据这16个字节中提供的信息,来确定应该怎样进一步解释和处理剩余头部中的48个字节。对于这16个字节的地址,include/linux/pci.h中定义了这样一些常数。

    #define PCI_VENDOR_ID  0x00 /* 16 bits */
    #define PCI_DEVICE_ID  0x02 /* 16 bits */
    #define PCI_COMMAND  0x04 /* 16 bits */

    #define PCI_STATUS  0x06 /* 16 bits */

    #define PCI_CLASS_REVISION 0x08 /* High 24 bits are class, low 8  revision */
    #define PCI_REVISION_ID         0x08    /* Revision ID */
    #define PCI_CLASS_PROG          0x09    /* Reg. Level Programming Interface */
    #define PCI_CLASS_DEVICE        0x0a    /* Device class */

    #define PCI_CACHE_LINE_SIZE 0x0c /* 8 bits */
    #define PCI_LATENCY_TIMER 0x0d /* 8 bits */
    #define PCI_HEADER_TYPE  0x0e /* 8 bits */

        对应我们的图3(见下)中的前16字节。而且我们也看到了紧挨着PCI_HEADER_TYPE (即存放头部类型的寄存器)下面定义的就是上面提到的三种类型的头部:

    #define  PCI_HEADER_TYPE_NORMAL   0
    #define  PCI_HEADER_TYPE_BRIDGE      1
    #define  PCI_HEADER_TYPE_CARDBUS  2

        在Linux系统上,可以通过cat /proc/pci  等命令查看系统中所有PCI设备的类别、型号以及厂商等等信息,那就是从这些寄存器来的。下面是在虚拟机中用lspci -x命令的信息截取(lspci命令也是使用/proc文件作为其信息来源):

    00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
    00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00
    10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19
    30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

        首先要说明的是PCI寄存器是小端字节序格式的。那么根据最下面的PCI配置寄存器组的结构(图 3),显然这个Host bridge的Vendor ID是0x8086,我不说你也能猜到这个Vendor就是Intel了。

        这里有个问题要先说清楚,就是这些寄存器的地址问题,不然往后就进行不下去了。配置寄存器可以让我们来进行配置以便完成PCI设备上的存储空间的访问,但这些配置寄存器本身也位于PCI设备地址空间中,如何访问这部分空间也就成了我们整个初始化工作的一个入口点,就像每个可执行程序都要有入口点一样。PCI采用的办法是让所有设备的配置寄存器组都采用相同的地址,由所在总线的PCI桥在访问时附加上其他条件来区分。而CPU则通过一个统一的入口地址向“宿主--PCI桥”发出命令,由相应的PCI桥间接的完成具体的读写。对于i386结构的处理器,PCI总线的设计者在I/O地址空间保留了8个字节用于这个目的,那就是0xCF8~0xCFF。这8个字节构成了两个32位的寄存器,第一个是“地址寄存器”0xCF8,第二个是“数据寄存器”0xCFC。要访问某个设备中的某个配置寄存器时,CPU先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据。不过,写入地址寄存器的目标地址是一种总线号、设备号、功能号以及设备寄存器地址在内的综合地址。格式如图2:

     

                                                         图2    写入地址寄存器0xCF8的综合地址

         这里的总线号、设备号和功能号是对配置寄存器地址的扩充,就是上面提到的附加的其他条件。首先每个PCI总线都有个总线号,主总线的总线号为0,其余的则由CPU在枚举阶段每当探测到一个PCI桥时便为其指定一个,依次递增。设备号一般代表着一块PCI接口卡(更确切的说是PCI总线接口芯片),通常取决于插槽的位置。每块PCI接口卡上可以有若干个功能模块,这些功能模块共用一个PCI总线接口芯片,包括其中用于地址映射的电子线路,以降低成本。从逻辑的角度说,每个“功能”实际上就是一个设备(看过USB设备驱动的人很眼熟吧 呵呵),所以设备号和功能号合在一起又可以称作“逻辑设备号”,而每块卡上最多可以容纳8个设备。显然,这些字段(指整个32bit)结合在一起就惟一确定了系统中的一项PCI逻辑设备。开始时,只有0号总线可以访问,在扫描0号总线时如果发现上面某个设备是PCI桥,就为之指定一个新的总线号,例如1,这样1号总线就可以访问了,这就是枚举阶段的任务之一。

        现在请读者考虑一个问题:当我们拿到一块PCI网卡,我们把它插到PC的主板上,打算写个这个网卡的驱动。那么第一步该干什么呢?读者可以回顾前面的内容,既然我们说Linux内核帮我们做了设备的枚举和配置工作,那么我在写网卡驱动之前是不是可以先看看Linux内核对我们的这个PCI网卡设备完成的枚举工作的结果呢?或者直白些说,我把网卡插上了,现在Linux内核有没有识别出这块设备呢? 注意识别出设备跟能正常使用设备是不同的概念,这很好理解。安装过PC网卡驱动的人都知道,当设备的驱动没有安装时,我们在设备管理器中是可以看到这个设备的,不过上面是一个黄色的大问号。而在Linux系统中,我们可以通过lspci命令来查看。

         下面是在LDD3的PCI驱动那一章截取的一段内容:  lspci 的输出( pciutils 的一部分, 在大部分发布中都有)和在 /proc/pci 和 /porc/bus/pci 中的信息排布. PCI 设备的 sysfs 表示也显示了这种寻址方案, 还有 PCI 域信息. 当显示硬件地址时, 它可被显示为 2 个值( 一个 8-位总线号和一个 8-位 设备和功能号), 作为 3 个值( bus, device, 和 function), 或者作为 4 个值(domain, bus, device, 和 function); 所有的值常常用 16 进制显示.

    例如, /proc/bus/pci/devices 使用一个单个16位字段(来便于分析和排序), 而 /proc/bus/busnumber 划分地址为3个字段. 下面内容显示了这些地址如何显示, 只显示了输出行的开始:

    $ lspci | cut -d: -f1-3
    0000:00:00.0 Host bridge
    0000:00:00.1 RAM memory
    0000:00:00.2 RAM memory
    0000:00:02.0 USB Controller
    0000:00:04.0 Multimedia audio controller
    0000:00:06.0 Bridge
    0000:00:07.0 ISA bridge
    0000:00:09.0 USB Controller
    0000:00:09.1 USB Controller
    0000:00:09.2 USB Controller
    0000:00:0c.0 CardBus bridge
    0000:00:0f.0 IDE interface
    0000:00:10.0 Ethernet controller
    0000:00:12.0 Network controller
    0000:00:13.0 FireWire (IEEE 1394)
    0000:00:14.0 VGA compatible controller
    $ cat /proc/bus/pci/devices | cut -f1
    0000
    0001
    0002
    0010
    0020
    0030
    0038
    0048
    0049
    004a
    0060
    0078
    0080
    0090
    0098
    00a0
    $ tree /sys/bus/pci/devices/
    /sys/bus/pci/devices/
    |-- 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0
    |-- 0000:00:00.1 -> ../../../devices/pci0000:00/0000:00:00.1
    |-- 0000:00:00.2 -> ../../../devices/pci0000:00/0000:00:00.2
    |-- 0000:00:02.0 -> ../../../devices/pci0000:00/0000:00:02.0
    |-- 0000:00:04.0 -> ../../../devices/pci0000:00/0000:00:04.0
    |-- 0000:00:06.0 -> ../../../devices/pci0000:00/0000:00:06.0
    |-- 0000:00:07.0 -> ../../../devices/pci0000:00/0000:00:07.0
    |-- 0000:00:09.0 -> ../../../devices/pci0000:00/0000:00:09.0
    |-- 0000:00:09.1 -> ../../../devices/pci0000:00/0000:00:09.1
    |-- 0000:00:09.2 -> ../../../devices/pci0000:00/0000:00:09.2
    |-- 0000:00:0c.0 -> ../../../devices/pci0000:00/0000:00:0c.0
    |-- 0000:00:0f.0 -> ../../../devices/pci0000:00/0000:00:0f.0
    |-- 0000:00:10.0 -> ../../../devices/pci0000:00/0000:00:10.0
    |-- 0000:00:12.0 -> ../../../devices/pci0000:00/0000:00:12.0
    |-- 0000:00:13.0 -> ../../../devices/pci0000:00/0000:00:13.0
    `-- 0000:00:14.0 -> ../../../devices/pci0000:00/0000:00:14.0
    

    所有的 3 个设备列表都以相同顺序排列, 因为 lspci 使用 /proc 文件作为它的信息源. 拿 VGA 视频控制器作一个例子, 0x00a0 意思是 0000:00:14.0 当划分为域(16位), 总线(8位), 设备(5位)和功能(3位).为什么0x00a0对应的是0000:00:14.0呢,这就要看图2中的内容了,根据图2中的寄存器对应0x00a0就代表着总线(8位), 设备(5位)和功能(3位).0x00a0=0000000010100000,很容易看出高8位是总线号也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能号,那么剩下的10100就是设备号,补全成8位的值就是00010100即0x14.

       

       

                                                                          图3    PCI配置寄存器组 

      

    参考资料:

    [1] Linux内核源代码情景分析(下册)

    [2] Linux设备驱动开发详解

    [3] Linux设备驱动(第三版)

    [4] 内核Documentation下的pci.txt

    [5] 精通Linux设备驱动开发

    [6] http://tldp.org/LDP/tlk/dd/pci.html

    [7] http://linux.die.net/man/8/lspci

    [8] http://www.ibm.com/developerworks/cn/linux/l-pci/

  • 相关阅读:
    安装配置ssh免密码登录
    大数据学习之Linux环境搭建(导航)
    Linux下搭建sqli-labs环境
    SpringMVC freemarker 中 Could not resolve view with name 'XXX.ftl' in servlet with name 'SpringMVC'
    配置FreeMarker时IDEA提示cannot resolve property 'templateLoaderPath'
    MySQL在指定字段后添加一个新字段
    META-INF/MANIFEST.MF file not found in unnamed.war
    Java获取音频播放时长
    JS实现阿拉伯数字转韩文
    微信公众号开发-素材管理-调用接口返回结果一览表
  • 原文地址:https://www.cnblogs.com/huty/p/8518630.html
Copyright © 2020-2023  润新知