PCI配置空间简介

简介: 一、PCI配置空间简介PCI有三个相互独立的物理地址空间:设备存储器地址空间、I/O地址空间和配置空间。配置空间是PCI所特有的一个物理空间。由于PCI支持设备即插即用,所以PCI设备不占用固定的内存地址空间或I/O地址空间,而是由操作系统决定其映射的基址。

一、PCI配置空间简介

  • PCI有三个相互独立的物理地址空间:设备存储器地址空间、I/O地址空间和配置空间。配置空间是PCI所特有的一个物理空间。由于PCI支持设备即插即用,所以PCI设备不占用固定的内存地址空间或I/O地址空间,而是由操作系统决定其映射的基址。

  • 系统加电时,BIOS检测PCI总线,确定所有连接在PCI总线上的设备以及它们的配置要求,并进行系统配置。所以,所有的PCI设备必须实现配置空间,从而能够实现参数的自动配置,实现真正的即插即用。

  • PCI总线规范定义的配置空间总长度为256个字节,配置信息按一定的顺序和大小依次存放。前64个字节的配置空间称为配置头,对于所有的设备都一样,配置头的主要功能是用来识别设备、定义主机访问PCI卡的方式(I/O访问或者存储器访问,还有中断信息)。其余的192个字节称为本地配置空间(设备有关区),主要定义卡上局部总线的特性、本地空间基地址及范围等。

  • PCI设备有三个空间——内存地址空间、IO地址空间和配置空间。由于PCI支持即插即用,所以PCI设备不是占用固定的内存地址空间或I/O地址空间,而是可以由操作系统决定其映射的基址。怎么配置呢?这就是配置空间的作用。


img_c58b094a8ec5cdc8c7ad8d2ca34b04ba.jpe


配置空间中最重要的有:

Vendor ID:厂商ID。知名的设备厂商的ID。FFFFh是一个非法厂商ID,可它来判断PCI设备是否存在。

Device ID:设备ID。某厂商生产的设备的ID。操作系统就是凭着 Vendor ID和Device ID 找到对应驱动程序的。

Class Code:类代码。共三字节,分别是 类代码、子类代码、编程接口。类代码不仅用于区分设备类型,还是编程接口的规范,这就是为什么会有通用驱动程序。

IRQ Line:IRQ编号。PC机以前是靠两片8259芯片来管理16个硬件中断。现在为了支持对称多处理器,有了APIC(高级可编程中断控制器),它支持管理24个中断。

IRQ Pin:中断引脚。PCI有4个中断引脚,该寄存器表明该设备连接的是哪个引脚。



二、如何访问配置空间

如何访问配置空间呢?可通过访问0xCF8h、0xCFCh端口来实现。

  • 0xCF8h: CONFIG_ADDRESS。PCI配置空间地址端口。
  • 0xCFCh: CONFIG_DATA。PCI配置空间数据端口。

CONFIG_ADDRESS寄存器格式:

31 位: Enabled位。
23:16 位: 总线编号。
15:11 位: 设备编号。
10: 8 位:功能编号。
7: 2 位:配置空间寄存器编号。
1: 0 位:恒为“00”。这是因为CF8h、CFCh端口是32位端口。

img_4f0ca9622f597756632dffbad9cd4a6a.jpe

现在有个难题——CF8h、CFCh端口是32位端口,可像Turbo C之类的16位C语言编译器都不支持32位端口访问。怎么办?我们可以使用**_ _ emit _ 在程序中插入机器码。每次都 _ emit _ _一下肯定很麻烦,所以我们应该将它封装成函数。代码如下(注意66h是32位指令前缀)**:

/* 读32位端口 */
DWORD inpd(int portid)
{
    DWORD dwRet;
    asm mov dx, portid;
    asm lea bx, dwRet;
    __emit__
    (0x66, 0x50,       // push EAX
    0x66, 0xED,        // in EAX,DX
    0x66, 0x89, 0x07,  // mov [BX],EAX
    0x66, 0x58);       // pop EAX
    return dwRet;
}
/* 写32位端口 */
void outpd(int portid, DWORD dwVal)
{
    asm mov dx, portid;
    asm lea bx, dwVal;
    __emit__
    (0x66, 0x50,       // push EAX
    0x66, 0x8B, 0x07,  // mov EAX,[BX]
    0x66, 0xEF,        // out DX,EAX
    0x66, 0x58);       // pop EAX
    return;
}


三、遍历PCI设备

怎么枚举PCI设备呢?我们可以尝试所有的 bus/dev/func 组合,然后判断得到的厂商ID是否为FFFFh。下面这个程序就是使用该方法枚举PCI设备的。同时为了便于分析数据,将每个设备的配置空间信息保存到文件,这样可以慢慢分析。


Windows下代码如下:

#include <stdio.h>
#include <conio.h>

typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef unsigned long DWORD;

/* PCI设备索引。bus/dev/func 共16位,为了方便处理可放在一个WORD中 */
#define PDI_BUS_SHIFT        8
#define PDI_BUS_SIZE         8
#define PDI_BUS_MAX          0xFF
#define PDI_BUS_MASK         0xFF00
#define PDI_DEVICE_SHIFT     3
#define PDI_DEVICE_SIZE      5
#define PDI_DEVICE_MAX       0x1F
#define PDI_DEVICE_MASK      0x00F8
#define PDI_FUNCTION_SHIFT   0
#define PDI_FUNCTION_SIZE    3
#define PDI_FUNCTION_MAX     0x7
#define PDI_FUNCTION_MASK    0x0007
#define MK_PDI(bus,dev,func) (WORD)((bus&PDI_BUS_MAX)<<PDI_BUS_SHIFT | (dev&PDI_DEVICE_MAX)<<PDI_DEVICE_SHIFT | (func&PDI_FUNCTION_MAX) )

/* PCI配置空间寄存器 */
#define PCI_CONFIG_ADDRESS   0xCF8
#define PCI_CONFIG_DATA      0xCFC

/* 填充PCI_CONFIG_ADDRESS */
#define MK_PCICFGADDR(bus,dev,func) (DWORD)(0x80000000L | (DWORD)MK_PDI(bus,dev,func) << 8)

/* 读32位端口 */
DWORD inpd(int portid) 
{
    DWORD dwRet;
    asm mov dx, portid;
    asm lea bx, dwRet;
    __emit__(
    0x66,0x50,      // push EAX
    0x66,0xED,      // in EAX,DX
    0x66,0x89,0x07, // mov [BX],EAX
    0x66,0x58);     // pop EAX
    return dwRet;
}

/* 写32位端口 */
void outpd(int portid, DWORD dwVal)
{
    asm mov dx, portid;
    asm lea bx, dwVal;
    __emit__(
    0x66,0x50,      // push EAX
    0x66,0x8B,0x07, // mov EAX,[BX]
    0x66,0xEF,      // out DX,EAX
    0x66,0x58);     // pop EAX
    return;
}

int main(void)
{
    int bus, dev, func;
    int i;
    DWORD dwAddr;
    DWORD dwData;
    FILE* hF;
    char szFile[0x10];
    printf("\n");
    printf("Bus#\tDevice#\tFunc#\tVendor\tDevice\tClass\tIRQ\tIntPin\n");
    /* 枚举PCI设备 */
    for(bus = 0; bus <= PDI_BUS_MAX; ++bus)
    {
        for(dev = 0; dev <= PDI_DEVICE_MAX; ++dev)
        {
            for(func = 0; func <= PDI_FUNCTION_MAX; ++func)
            {
                /* 计算地址 */
                dwAddr = MK_PCICFGADDR(bus, dev, func);

                /* 获取厂商ID */
                outpd(PCI_CONFIG_ADDRESS, dwAddr);
                dwData = inpd(PCI_CONFIG_DATA);
                /* 判断设备是否存在。FFFFh是非法厂商ID */
                if ((WORD)dwData != 0xFFFF)
                {
                    /* bus/dev/func */
                    printf("%2.2X\t%2.2X\t%1X\t", bus, dev, func);
                    /* Vendor/Device */
                    printf("%4.4X\t%4.4X\t", (WORD)dwData, dwData>>16);
                    /* Class Code */
                    outpd(PCI_CONFIG_ADDRESS, dwAddr | 0x8);
                    dwData = inpd(PCI_CONFIG_DATA);
                    printf("%6.6lX\t", dwData>>8);
                    /* IRQ/intPin */
                    outpd(PCI_CONFIG_ADDRESS, dwAddr | 0x3C);
                    dwData = inpd(PCI_CONFIG_DATA);
                    printf("%d\t", (BYTE)dwData);
                    printf("%d", (BYTE)(dwData>>8));
                    printf("\n");
                    /* 写文件 */
                    sprintf(szFile, "PCI%2.2X%2.2X%X.bin", bus, dev, func);
                    hF = fopen(szFile, "wb");
                    if (hF != NULL)
                    {
                        /* 256字节的PCI配置空间 */
                        for (i = 0; i < 0x100; i += 4)
                        {
                            /* Read */
                            outpd(PCI_CONFIG_ADDRESS, dwAddr | i);
                            dwData = inpd(PCI_CONFIG_DATA);
                            /* Write */
                            fwrite(&dwData, sizeof(dwData), 1, hF);
                        }
                        fclose(hF);
                    }
                }
            }
        }
    }
    return 0;
}

总线编号为0的都是主板上固有的芯片(主要是南桥),非主板设备的典型是——显卡。WindowsXP的设备管理器中也可以看到PCI信息。启动“设备管理器”,最好将查看方式设为“依连接查看设备(V)”。找到我的显卡,双击查看属性。切换到“详细信息”页,定位组合框为“硬件Id”。可看到其中一行为“PCI/VEN_10DE&DEV_0110&CC_030000”,表示厂商ID为“10DE”、设备ID为“0110”、类代码为“030000”,与程序得到的结果一致。



linux下代码如下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/io.h>

#define PCI_MAX_BUS 255
#define PCI_MAX_DEV 31
#define PCI_MAX_FUN 7

#define PCI_BASE_ADDR 0x80000000L

#define CONFIG_ADDR 0xcf8
#define CONFIG_DATA 0xcfc

typedef unsigned long DWORD;
typedef unsigned int WORD;


int main()
{
    WORD bus, dev, fun;
    DWORD addr, data;

    printf("\nbus#\tdev#\tfun#\tvendor#\t\tdevice#\n");

    if ( iopl(3) < 0 )
    {
        printf("iopl set error\n");
        return -1;
    }
    
    for (bus = 0; bus <= PCI_MAX_BUS; bus++)
        for (dev = 0; dev <= PCI_MAX_DEV; dev++)
            for (fun = 0; fun <= PCI_MAX_FUN; fun++)
            {
                addr = PCI_BASE_ADDR | (bus << 16) | (dev << 11) | (fun << 8);
                
                outl(addr, CONFIG_ADDR);
                data = inl(CONFIG_DATA);
                
                if (((data & 0xFFFF) != 0xFFFF) && (data != 0))
                {
                    // bus, dev, fun
                    printf("%02d  \t%02d  \t%02d  \t", bus, dev, fun);
            
                    // vendorID、deviceID
                    printf("%04x  \t\t%04x", (data & 0xFFFF), (data & 0xFFFF0000) >> 16);
                    
                    printf("\n--------------------------------------------\n");
                }
            }

    if (iopl(0) < 0 )
    {
        printf("iopl set error\n");
        return -1;
    }
    
    return 0;

}
目录
相关文章
|
4月前
|
Linux 开发工具
【ZYNQ】配置嵌入式 Linux 静态 IP 地址
【ZYNQ】配置嵌入式 Linux 静态 IP 地址
221 0
|
9月前
|
C语言 Windows
9.3 Windows驱动开发:内核解析PE结构节表
在笔者上一篇文章`《内核解析PE结构导出表》`介绍了如何解析内存导出表结构,本章将继续延申实现解析PE结构的PE头,PE节表等数据,总体而言内核中解析PE结构与应用层没什么不同,在上一篇文章中`LyShark`封装实现了`KernelMapFile()`内存映射函数,在之后的章节中这个函数会被多次用到,为了减少代码冗余,后期文章只列出重要部分,读者可以自行去前面的文章中寻找特定的片段。
43 0
9.3 Windows驱动开发:内核解析PE结构节表
|
9月前
|
编译器 C++ Windows
9.4 Windows驱动开发:内核PE结构VA与FOA转换
本章将继续探索内核中解析PE文件的相关内容,PE文件中FOA与VA,RVA之间的转换也是很重要的,所谓的FOA是文件中的地址,VA则是内存装入后的虚拟地址,RVA是内存基址与当前地址的相对偏移,本章还是需要用到`《内核解析PE结构导出表》`中所封装的`KernelMapFile()`映射函数,在映射后对其PE格式进行相应的解析,并实现转换函数。
55 0
9.4 Windows驱动开发:内核PE结构VA与FOA转换
|
Linux 项目管理 芯片
实战 | RISC-V Linux入口地址2M预留内存优化
实战 | RISC-V Linux入口地址2M预留内存优化
|
程序员
【操作系统】第五章:虚拟内存(Part1:交换[swapping]和覆盖[overlay]技术)
【操作系统】第五章:虚拟内存(Part1:交换[swapping]和覆盖[overlay]技术)
548 0
RK3399平台开发系列讲解(高速设备驱动篇)6.54、PCIe对PCI配置空间的扩展
RK3399平台开发系列讲解(高速设备驱动篇)6.54、PCIe对PCI配置空间的扩展
343 0
RK3399平台开发系列讲解(高速设备驱动篇)6.54、PCIe对PCI配置空间的扩展
RK3399平台开发系列讲解(PCI/PCI-E)5.53、PCIE RC侧 地址映射
RK3399平台开发系列讲解(PCI/PCI-E)5.53、PCIE RC侧 地址映射
195 0
RK3399平台开发系列讲解(PCI/PCI-E)5.53、PCIE RC侧 地址映射
|
固态存储 Linux 内存技术
浅析PCI配置空间
每个Function都有一个大小为4KB的configuration space。在系统上电的过程中,在枚举整个PCI Bus之后,就会将所有的BDF的configuration space读到Host内存中。在Host内存中有一个大小256MB的Memory Block, 专门用来存放所有的configuration space.