Linux学习之路嵌入式 Linux C ARM Linux

PCI设备驱动(一)

2019-01-03  本文已影响3人  Leon_Geo

首先要明确两个概念:Linux内核 PCI设备驱动和设备本身驱动两部分。工作中所谓的编写设备驱动,其实就是编写设备本身驱动。因为Linux 内核的PCI驱动是内核自带的。

当然,并不是说内核帮咱们写好了Linux PCI驱动我们什么就不用做了,至少你要明白内核大致都干了些什么,这样你才能明白你该干什么,如何完成设备本身的驱动。我们下面就来研究下Linux PCI驱动到底都干了些什么。

Linux PCI 初始化代码逻辑上分为三个部分:

(1)内核的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总线称为次总线,即主总线的子总线。
图1 PCI系统示意图

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


图2 Base class 06h

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桥建立起的硬件设备树,我有内存中通过数据结构构建的软件树,很和谐。

配置: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有三种地址空间,其中的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配置寄存器组的结构(图4),显然这个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先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据。不过,写入地址寄存器的目标地址是一种总线号、设备号、功能号以及设备寄存器地址在内的综合地址。格式如下图: 图3 写入地址寄存器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

所有的 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. 图4 PCI配置寄存器组
上一篇 下一篇

猜你喜欢

热点阅读