初识PCI

话说,小弟接触计算机(不是“电脑”)也有很多个年头了,很多名词也能朗朗上口,但仅仅限于名词,对其中的原理却没有深入的了解。像MS DOS,其实也是前段时间才真正“安装”了一个用来更新设备的BIOS。而如USB、ISA、PCI这些简称,同样地不了解,以至于不敢与人谈自己是嵌入式工程师。大学时没有计算接口这一类的课程,自己找书看了,但发现里面讲的和平时所学的差别太大,又感觉自己后面不会用到,所以也就囫囵吞枣一遍,后来终于还是有所接触,只不过徒知其名,不知其理。

去年年底移植了一个x86平台的SPI flash的驱动——其实是参考开源项目,加以修改整合到实际应用中。一开始就接触了PCI,因为SPI寄存器基地址就是从PCI配置空间中拿到的。于是上网找资料。还借了本公司的PCI局部总线原理的书来看。打开那本书,发现都是讲硬件电气特性、通信协议这些细节,对于配置空间却涉及很少。我只是想了解怎么从手上这块板子的PCI中拿到基地址。于是果然放弃书本——这也暴露了本人不求甚解的本质。然后决定还是从网络找资料,看代码、看配置空间,等等。慢慢有一点点了解。鉴于自己写文章一向是根据自己实际情况来写“流水账”,还是继续吧。
我所关心的PCI有2点:配置空间(pci configurationspace)是长怎么样的;如何获取到PCI设备。
网上有很多文章都讲有配置空间。下面是我从网上找到一张讲配置空间的图:

这个图只是表述了一般的内容,有些不在上述图中却有用处,这要具体看手册的说明才行。比如,前段时间搞的flash驱动,spi的寄存器基地址位于pci的0x54处。
访问PCI配置空间是通过2个IO端口来进行的(这点承认写得不到位,对微机接口了解不深)。
CONFIG_ADDRESS – 地址寄存器, PCI配置空间地址写到该寄存器,IO端口为CF8h。
CONFIG_DATA - 数据寄存器,来用读取PCI数据,IO端口为CFCh上述端口均为32位寄存器。
访问地址组成格式如下图:

有了配置空间的认识,有了访问的方法和地址,就可以访问PCI设备了。代码表示如下:

1
2
3
4
5
// 格式 31 Enabled 23:16 总线编号  15:11 设备编号 10:8 功能编号 7:2 配置空间寄存器编号 1:0 为0
io_addr = 0x80000000 + (bus<<16) + (device<<11) + (function<<8);
// 将地址写到CONFIG_ADDR寄存器
outl(io_addr, PCI_CONFIG_ADDR);
result = inl(PCI_CONFIG_DATA); // 读data数据寄存器,得到id

在Linux上,可以使用lspci命令(有的系统可能没安装lspci)来查找PCI设备。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
# lspci
00:00.0 Class 0600: Device 8086:0f00 (rev 0c)
00:02.0 Class 0300: Device 8086:0f31 (rev 0c)
...
00:1c.3 Class 0604: Device 8086:0f4e (rev 0c)
00:1d.0 Class 0c03: Device 8086:0f34 (rev 0c)
00:1f.0 Class 0601: Device 8086:0f1c (rev 0c)
00:1f.3 Class 0c05: Device 8086:0f12 (rev 0c)
01:00.0 Class 0200: Device 8086:1539 (rev 03)
02:00.0 Class 0200: Device 8086:1539 (rev 03)

lspci还可以列出具体某一个PCI设备的信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# lspci -v -s 00:1f.3 -xxx
00:1f.3 Class 0c05: Device 8086:0f12 (rev 0c)
Subsystem: Device 8086:7270
Flags: medium devsel, IRQ 11
Memory at d0816000 (32-bit, non-prefetchable) [size=32]
I/O ports at 3000 [size=32]
Capabilities: [50] Power Management version 3
00: 86 80 12 0f 03 00 90 02 0c 00 05 0c 00 00 00 00
10: 00 60 81 d0 00 00 00 00 00 00 00 00 00 00 00 00
20: 01 30 00 00 00 00 00 00 00 00 00 00 86 80 70 72
30: 00 00 00 00 50 00 00 00 00 00 00 00 0b 02 00 00
40: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50: 01 00 03 00 08 00 00 00 00 00 00 00 00 00 00 00
60: 03 04 04 00 00 00 08 08 00 00 00 00 00 00 00 00
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
80: 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
f0: 00 00 00 00 00 00 00 00 1a 0f 0c 01 03 01 00 00

注意,命令的“-xxx”就是“-xxx”,是一个命令选项,不是表示未知或按实际输入的意思。每一偏移的地址、数值都可以在里面找到。如果有闲情,可以对照着PCI配置空间来慢慢了解个中的数值的含义。
下面参照一个intel芯片手册来了解PCI。以SMBUS为具体例子,它的配置空间说明如下:

红框是我画的,是我很关心的字段。前面2个字段就是PCI的VID和DID,是只读的。通过它,就可以判断出是不是自己想要的PCI设备,因为系统中的PCI设备是很多的,需要根据ID来区别不同的设备。
设备ID(DevID)寄存器如下:

右下方面的RO表示是只读的。左上方红框的SMB_Config_DID: [B:0, D:31, F:3] + 2h表述了PCI设备地址和偏移量。B、D、F分别表示bus、device、function,它的值对应着前面用lspci查找到的地址。+2h表示偏移量。这在intel手册中经常见,我也是问了一个PCI高手才知道的。
参考网上资料写的一个遍历PCI设备的程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <inttypes.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/io.h> // inb/outb,etc...

#define PCI_CONFIG_ADDR 0xCF8
#define PCI_CONFIG_DATA 0xCFC

int pci_autodetect()
{
    uint32_t result;
    uint32_t io_addr;
    int function = 0;
    int bus = 0;
    int device = 0;
    int rev = 0;
    uint32_t vendor_id, dev_id, class1, class2, class3;

    for (bus = 0; bus<5; bus++)
    {
        for (device = 0; device<32; device++)
        {
            for(function = 0; function<8 ; function++ )
            {
                // 格式 31 Enabled 23:16 总线编号 15:11 设备编号 10:8 功能编号 7:2 配置空间寄存器编号 1:0 为0
                io_addr = 0x80000000 + (bus<<16) + (device<<11) + (function<<8);
                // 将地址写到CONFIG_ADDR寄存器
                outl(io_addr, PCI_CONFIG_ADDR);
                result = inl(PCI_CONFIG_DATA); // 读data数据寄存器,得到id
                if (result != 0xffffffff) // not the bad one print it
                {
                    vendor_id = result & 0xFFFF;
                    dev_id = result >> 16;
                    outl(io_addr+0x08, PCI_CONFIG_ADDR);
                    result = inl(PCI_CONFIG_DATA);
                    rev = result&0xff;
                    class3 = (result>>8)&0xff;
                    class2 = (result>>16)&0xff;
                    class1 = (result>>24)&0xff;
                    printf("0x%08x %02x:%02x.%x ", io_addr, bus, device, function);
                    //printf("PCI Read result: 0x%08x ioaddr: 0x%08x ", result, io_addr);
                    printf("Class %02x%02x: Device %04x:%04x (rev: %02x)\n", class1, class2, 
                                vendor_id, dev_id,rev);
                }
            }
        }
    }
    return 0;
}

int main(void)
{
    iopl(3); // 必须要这句
    pci_autodetect();
    return 0;
}

上面的程序,在虚拟机上运行结果的一个示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x80000000 00:00.0 Class 0600: Device 8086:7190 (rev: 01)
0x80000800 00:01.0 Class 0604: Device 8086:7191 (rev: 01)
0x80003800 00:07.0 Class 0601: Device 8086:7110 (rev: 08)
0x80003900 00:07.1 Class 0101: Device 8086:7111 (rev: 01)
0x80003b00 00:07.3 Class 0680: Device 8086:7113 (rev: 08)
0x80003f00 00:07.7 Class 0880: Device 15ad:0740 (rev: 10)
0x80007800 00:0f.0 Class 0300: Device 15ad:0405 (rev: 00)
0x80008000 00:10.0 Class 0100: Device 1000:0030 (rev: 01)
0x80008800 00:11.0 Class 0604: Device 15ad:0790 (rev: 02)
0x8000a800 00:15.0 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000a900 00:15.1 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000aa00 00:15.2 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000ab00 00:15.3 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000ac00 00:15.4 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000ad00 00:15.5 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000ae00 00:15.6 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000af00 00:15.7 Class 0604: Device 15ad:07a0 (rev: 01)
0x8000b000 00:16.0 Class 0604: Device 15ad:07a0 (rev: 01)

用lspci命令结果如下:

1
2
3
4
5
6
7
8
9
10
11
# lspci 
00:00.0 Host bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
00:01.0 PCI bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX AGP bridge (rev 01)
00:07.0 ISA bridge: Intel Corporation 82371AB/EB/MB PIIX4 ISA (rev 08)
00:07.1 IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)
00:07.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 08)
00:07.7 System peripheral: VMware Virtual Machine Communication Interface (rev 10)
00:0f.0 VGA compatible controller: VMware SVGA II Adapter
00:10.0 SCSI storage controller: LSI Logic / Symbios Logic 53c1030 PCI-X Fusion-MPT Dual Ultra320 SCSI (rev 01)
00:11.0 PCI bridge: VMware PCI bridge (rev 02)
00:15.0 PCI bridge: VMware PCI Express Root Port (rev 01)

对比可以发现,程序还没有做到获取PCI设备厂商描述信息。
本文不涉及到PCI电气特性,不涉及到linux内核深奥的框架,仅作为一个入门者的笔记。
参考资源:
PCI文章:http://blog.chinaunix.net/uid-618506-id-204331.html
http://blog.csdn.net/linuxdrivers/article/details/5849698
芯片手册(从搜索引擎可以搜索到的):http://www.intel.com/content/www/us/en/intelligent-systems/bay-trail/atom-e3800-family-datasheet.html

李迟,发表于2015年2月9日,中午