读取PCI配置空间数据并操作其映射的物理内存

PC机在启动的时候,都会看到一个PCI设备清单,可以看到机器中的所有PCI设备,其实搜索PCI设备的程序并不难编,本文通过一个实例说明如何遍历PCI设备。 1、了解PCI设备
PCI的含义是外设部件互连(Peripheral Component Interconnect),PCI局部总线(Local Bus)是1991年由Intel定义的,现在PCI局部总线已经成为了PC机中不可缺少的外围设备总线,几乎所有的外部设备都连接到PCI局部总线上, 我们说的PCI设备,实际上就是指连接在PCI局部总线上的设备。 2、了解PCI配置空间
学习PCI编程,不了解PCI的配置空间是不可能的,配置空间是一块容量为256字节并具有特定记录结构或模型的地址空间,通过配置空间,我们可以了解该PCI设备的一些配置情况,进而控制该设备,除主总线桥以外的所有PCI设备都必须事先配置空间,本节仅就一些配置空间的共有的规定作一些说明,更加具体和详细的信息请参阅其他书籍及相应的芯片手册。
配置空间的前64个字节叫头标区,头标区又分成两个部分,第一部分为前16个字节,在各种类型的设备中定义都是一样的,其他字节随各设备支持的功能不同而有所不同,位于偏移0EH的投标类型字段规定了是何种布局,目前有三种头标类型,头标类型1用于PCI-PCI桥,头标类型2用于PCI-CARDBUS 桥,头标类型0用于其他PCI设备,下图为头标类型0的头标区布局。

DW |Byte3|Byte2|Byte1|Byte0| Addr ---+---------------------------------------------------------+----- 0 |Device ID|Vendor ID| 00 ---+---------------------------------------------------------+----- 1 |Status|Command| 04 ---+---------------------------------------------------------+----- 2 |Class Code| Revision ID | 08 ---+---------------------------------------------------------+----- 3 |BIST| Header Type | Latency Timer | Cache Line| 0C ---+---------------------------------------------------------+----- 4 |Base Address 0| 10 ---+---------------------------------------------------------+----- 5 |Base Address 1| 14 ---+---------------------------------------------------------+----- 6 |Base Address 2| 18 ---+---------------------------------------------------------+----- 7 |Base Address 3| 1C ---+---------------------------------------------------------+----- 8 |Base Address 4| 20 ---+---------------------------------------------------------+----- 9 |Base Address 5| 24 ---+---------------------------------------------------------+----- 10 |CardBus CIS pointer| 28 ---+---------------------------------------------------------+----- 11 |Subsystem Device ID|Subsystem Vendor ID| 2C ---+---------------------------------------------------------+----- 12 |Expansion ROM Base Address| 30 ---+---------------------------------------------------------+----- 13 |Reserved(Capability List)| 34 ---+---------------------------------------------------------+----- 14 |Reserved| 38 ---+---------------------------------------------------------+----- 15 |Max_Lat|Min_Gnt|IRQ Pin|IRQ Line| 3C -------------------------------------------------------------------

头标区中有5个字段涉及设备的识别。
  1. 供应商识别字段(Vendor ID)该字段用一标明设备的制造者。一个有效的供应商标识由PCI SIG来分配,以保证它的唯一性。0FFFFH是该字段的无效值。
  2. 设备识别字段(Device ID)用以标明特定的设备,具体代码由供应商来分配。
  3. 版本识别字段(Revision ID)用来指定一个设备特有的版本识别代码,其值由供应商提供,可以是0。
  4. 头标类型字段(Header Type)该字段有两个作用,一是用来表示配置空间头标区第二部分的布局类型;二是用以指定设备是否包含多功能。位7用来标识一个多功能设备,位7为0表明是单功能设备,位7为1表明是多功能设备。位0-位6表明头标区类型。
  5. 分类代码字段(Class Code)标识设备的总体功能和特定的寄存器级编程接口。该字节分三部分,每部分占一个字节,第一部分是基本分类代码,位于偏移0BH,第二部分叫子分类代码,位于偏移0AH处,第三部分用于标识一个特定的寄存器级编程接口(如果有的话)。
3、配置空间寄存器的读写
x86的CPU只有内存和I/O两种空间,没有专用的配置空间,PCI协议规定利用特定的I/O空间操作驱动PCI桥路转换成配置空间的操作。目前存在两种转换机制,即配置机制1#和配置机制2#。配置机制2#在新的设计中将不再被采用,新的设计应使用配置机制1#来产生配置空间的物理操作。这种机制使用了两个特定的32位I/O空间,即CF8h和CFCh。这两个空间对应于PCI桥路的两个寄存器,当桥路看到CPU在局部总线对这两个I/O空间进行双字 操作时,就将该I/O操作转变为PCI总线的配置操作。寄存器CF8h用于产生配置空间的地址(CONFIG-ADDRESS),寄存器CFCh用于保存 配置空间的读写数据(CONFIG-DATA)。
将要访问配置空间寄存器的总线号、设备号、功能号和寄存器号以一个双字的格式写到配置地址端口 (CF8H-CFBH),接着执行配置数据端口(CFCH)的读和写,向配置数据口写数据即向配置空间写数据,从配置数据口读数据即从配置空间读数据。
配置地址端口(CF8H)的格式定义如下:
寄存器号:选择配置空间中的一个双字(32位) 功能号:选择多功能设备中的某一个功能,有八种功能,0--7 设备号:在一条给定的总线上选择32个设备中的一个。0--31 总线号:从系统中的256条总线中选择一条,0--255

尽管理论上可以有256条总线,但实际上PC机上PCI插槽的总线号都是1,有些工控机的总线号是2或者3,所以我们只需要查找0--4号总线就足够了。PCI规范规定,功能0是必须实现的,所以,如果功能0的头标类型字段的位7为0,表明这是一个单功能设备,则没有必要再去查其他功能,否则要查询所有其他功能。
4、读取PCI设备
至此,我们掌握的有关PCI的知识已经足够我们遍历PCI设备了,其实便利方法非常简单就是按照总线号、设备号、功能号的顺序依次罗列所有的可能性,读取配置空间头标区的供应商代码、及设备代码,进而找到所有PCI设备。下面是一个例子,关于读取指定PCI设备的指定配置空间数据的方法,同时可以根据读取的BAR值操作物理内存。 [cpp]view plain copy
  1. #include
  2. #include
  3. #include
  4. #include
  5. #include
  6. #include
  7. #include
  8. #include
  9. #include
  10. #include
  11. #define PCI_CONFIG_ADDR(bus, dev, fn, reg) (0x80000000 | (bus << 16) | (dev << 11) | (fn << 8) | (reg & ~3))
  12. void usage()
  13. {
  14. printf("Usage: readpci [-bdfrth]\n\
  15. -aaddr:specify bar address default 0\n\
  16. -bbus:specifyPCIbusnumber(default0)\n\
  17. -ddev:devicenumber(default0)\n\
  18. -ffn:functionnumber(default0)\n\
  19. -rreg:registeraddress(mustbemultipleof4,default0)\n\
  20. -pport:specify port number default 0\n\
  21. -vvalue :write a integer value into the address\n\
  22. -h:printthishelptext\n ");
  23. exit(-1);
  24. }
  25. int operaMem(int bar, int offset, int modValue, int isWrite)
  26. {
  27. int i;
  28. int fd;
  29. char* mem;
  30. //open /dev/mem with read and write mode
  31. if((fd = open ("/dev/mem", O_RDWR)) < 0)
  32. {
  33. perror ("open error\n");
  34. return -1;
  35. }
  36. //map physical memory 0-10 bytes
  37. mem = mmap (0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, bar);
  38. if (mem == MAP_FAILED)
  39. {
  40. perror ("mmap error:\n");
  41. return 1;
  42. }
  43. int value = https://www.it610.com/article/*((int *)(mem + offset));
  44. printf("The value at 0x%x is 0x%x\n", bar + offset, value);
  45. if(isWrite == 1)
  46. {
  47. printf("Write value 0x%x at 0x%x\n", modValue, bar + offset);
  48. memcpy(mem + offset, &modValue, 4);
  49. printf("Reread the value at 0x%x is 0x%x\n", bar + offset, *((int *)(mem + offset)));
  50. }
  51. munmap (mem, 4096); //destroy map memory
  52. close (fd); //close file
  53. return 0;
  54. }
  55. int parseInt(char *str)
  56. {
  57. int value = https://www.it610.com/article/0;
  58. char tmpChar;
  59. while((tmpChar = *str) != '\0')
  60. {
  61. if(tmpChar >= 'A' && tmpChar <= 'F')
  62. {
  63. tmpChar = tmpChar - 'A' + 10;
  64. }
  65. else if(tmpChar >= 'a' && tmpChar <= 'f')
  66. {
  67. tmpChar = tmpChar - 'a' + 10;
  68. }
  69. else
  70. {
  71. tmpChar -= '0';
  72. }
  73. value = https://www.it610.com/article/value * 16 + tmpChar;
  74. str += 1;
  75. }
  76. return value;
  77. }
  78. int main(int argc, char **argv)
  79. {
  80. unsigned long val = 0;
  81. char options[] = "a:b:d:f:r:v:p:h";
  82. int addr = 0, bus = 0, dev = 0, fn = 0, reg = 0, port = 0;
  83. int opt, value = https://www.it610.com/article/0, isWrite = 0, isReadBar = 0;
  84. while((opt = getopt(argc, argv, options)) != -1)
  85. {
  86. switch(opt)
  87. {
  88. case 'a':
  89. addr = parseInt(optarg);
  90. break;
  91. case 'b':
  92. bus = atoi(optarg);
  93. break;
  94. case 'd':
  95. dev = atoi(optarg);
  96. break;
  97. case 'f':
  98. fn = atoi(optarg);
  99. break;
  100. case 'r':
  101. reg = parseInt(optarg);
  102. break;
  103. case 'p':
  104. port = atoi(optarg);
  105. isReadBar = 1;
  106. break;
  107. case 'v':
  108. value = https://www.it610.com/article/parseInt(optarg);
  109. isWrite = 1;
  110. break;
  111. case 'h':
  112. default:
  113. usage();
  114. break;
  115. }
  116. }
  117. iopl(3);
  118. if(isWrite == 0)
  119. {
  120. if(isReadBar == 0)
  121. {
  122. int i;
  123. for(i = 0; i < 256; i += 4)
  124. {
  125. outl(PCI_CONFIG_ADDR(bus, dev, fn, i), 0xCF8);
  126. val = inl(0xCFC);
  127. printf("PCI:Bus %d, DEV %d, FUNC %d, REG %x, Value is %x\n ", bus, dev, fn, i, val);
  128. }
  129. }
  130. else
  131. {
  132. outl(PCI_CONFIG_ADDR(bus, dev, fn, 0x10), 0xCF8);
  133. val = inl(0xCFC) & 0xfffffff0;
  134. printf("The base address value is 0x%x\n", val);
  135. int pointAddr = val + port * 0x1000;
  136. printf("The offset address value is 0x%x\n", pointAddr + addr);
  137. operaMem(pointAddr, addr, 0, 0);
  138. }
  139. }
  140. else
  141. {
  142. outl(PCI_CONFIG_ADDR(bus, dev, fn, 0x10), 0xCF8);
  143. val = inl(0xCFC) & 0xfffffff0;
  144. printf("The base address value is 0x%x\n", val);
  145. int pointAddr = val + port * 0x1000;
  146. printf("The offset address value is 0x%x\n", pointAddr + addr);
  147. operaMem(pointAddr, addr, value, 1);
  148. }
  149. return 0;
  150. }
====================================================================================================== PCI信息的读写 应用程序中pci配置空间读写(2010-04-28 15:22:35) 标签:it 分类: linux和嵌入式 应用程序中pci配置空间读写
~~~~~~~~~~~~~~~~~~~~~~~
1. 概述
主要讲述在linux操作系统中应用程序如何读写pci配置空间。

2. 基本原理
对于pci设备,由cpu通过一个统一的入口地址向pci总桥发出命令,再由相应的pci桥间接地完成具体的读写操作。对于X86结构,pci总线的设计者在I/O地址空间保留了8个字节用于这个操作:0xcf8 - 0xcff。这8个字节分成两个32位的寄存器,0xcf8是地址寄存器,0xcfc是数据寄存器。cpu先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据,写入地址寄存器中的目标地址是一种包括总线号、设备号、功能号以及配置寄存器地址的综合地址。具体如下:
bit31 1
bit30-bit24 保留
bit23-bit16 总线号
bit15-bit11 设备号
【读取PCI配置空间数据并操作其映射的物理内存】 bit10-bit8 功能号
bit7-bit0 寄存器地址,最低两位为0
设备的这些信息都可以通过lspci命令获取。

3. ioport读写权限
由于要通过操作ioport,所以先要获取ioport的读写权限。
能获取读写权限的函数有两个ioperm()和iopl()。
由于pci的口地址比较高,要用iopl(3)获取权限。

4. 读程序
#define PCI_CFG_DATA 0xcfc
#define PCI_CFG_CTRL 0xcf8

void pci_read_config_byte(unsigned char bus, unsigned char dev, unsigned char offset, unsigned char *val)
{
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) | ((fun)<<8) | (offset & ~0x3)), PCI_CFG_CTRL);
*val = inl(PCI_CFG_DATA) >> ((offset & 3) * 8);
}

void pci_read_config_word(unsigned char bus, unsigned char dev, unsigned char offset, unsigned short *val)
{
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) | ((fun)<<8) | (offset & ~0x3)), PCI_CFG_CTRL);
*val = inl(PCI_CFG_DATA) >> ((offset & 3) * 8);
}

void pci_read_config_dword(unsigned char bus, unsigned char dev, unsigned char offset, unsigned int *val)
{
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) | ((fun)<<8) | (offset)), PCI_CFG_CTRL);
*val = inl(PCI_CFG_DATA);
}
很明显就是先向控制寄存器写入综合地址,格式前面已经提到,对比一下是完全一样的。然后从数据寄存器读数据即可,由于数据寄存器是32位的,如果不是读取双字,需要做移位操作。
另外一定需要注意大小端问题,如需要就要进行大小端转换,下面写程序也一样。

5. 写程序
来源:(http://blog.sina.com.cn/s/blog_559f6ffc0100i95s.html) - 应用程序中pci配置空间读写_云月_新浪博客 void pci_write_config_dword(unsigned char bus, unsigned char dev, unsigned char offset, unsigned int val)
{
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) | ((fun)<<8) | (offset)), PCI_CFG_CTRL);
outl(val, PCI_CFG_DATA);
}

void pci_write_config_word(unsigned char bus, unsigned char dev, unsigned char offset, unsigned short val)
{
unsigned long tmp;
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) | ((fun)<<8) | (offset & ~0x3)), PCI_CFG_CTRL);
tmp = inl(PCI_CFG_DATA);
tmp &= ~(0xffff << ((offset & 0x3) * 8));
tmp |= (val << ((offset & 0x3) * 8));
outl(tmp, PCI_CFG_DATA);
}

void pci_write_config_byte(unsigned char bus, unsigned char dev, unsigned char offset, unsigned short val)
{
unsigned long tmp;
unsigned char fun = 0;

outl((0x80000000 | ((bus)<<16) |((dev)<<11) |((fun)<<8) | (offset & ~0x3)), PCI_CFG_CTRL);
tmp = inl(PCI_CFG_DATA);
tmp &= ~(0xff << ((offset & 0x3) * 8));
tmp |= (val << ((offset & 0x3) * 8));
outl(tmp, PCI_CFG_DATA);
}
写程序同读程序一样,先向控制寄存器写入综合地址,然后向数据寄存器写入数据。

6. 问题
上面的程序都是参考linux内核对pci空间的读写程序写的。但是在应用程序中读写pci空间和在内核中读写pci空间是完全不同的。在linux源代码中可以看到,在进行pci空间的读写操作都是在关闭中断的情况下进行的,而在用户程序空间就没有这个手段了。所以,读写可能会出错。
经过本人试验,读基本上没有出错过,而写有一定出错的概率,慎用!
有兴趣的,可以随便写个应用程序试试看。

7. 源代码
附上一份源代码,可以直接编译运行。
#include
#include
#include
static unsigned int read_pci_config_32(unsigned char bus, unsigned char slot, unsigned char func, unsigned char offset)
{
unsigned int v;
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
v = inl(0xcfc);
return v;
}
unsigned char read_pci_config_8(unsigned char bus, unsigned char slot, unsigned char func, unsigned char offset)
{
unsigned char v;
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
v = inb(0xcfc + (offset&3));
return v;
}
unsigned short read_pci_config_16(unsigned char bus, unsigned char slot, unsigned char func, unsigned char offset)
{
unsigned short v;
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
v = inw(0xcfc + (offset&2));
return v;
}
void write_pci_config_32(unsigned char bus, unsigned char slot, unsigned char func, unsigned char offset, unsigned int val)
{
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
outl(val, 0xcfc);
}
void write_pci_config_8(unsigned char bus,unsigned char slot, unsigned char func, unsigned char offset, unsigned char val)
{
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
outb(val, 0xcfc + (offset&3));
}
void write_pci_config_16(unsigned char bus,unsigned char slot, unsigned char func, unsigned char offset, unsigned char val)
{
outl(0x80000000 | (bus<<16) | (slot<<11) | (func<<8) | offset, 0xcf8);
outw(val, 0xcfc + (offset&2));
}
int main(void)
{
iopl(3);

printf("0 0 0 0 = %x\n", read_pci_config_16(0, 0 , 0, 0));
printf("0 0 0 2 = %x\n", read_pci_config_16(0, 0 , 0, 2));
printf("0 1 0 0 = %x\n", read_pci_config_16(0, 1 , 0, 0));
printf("0 1 0 2 = %x\n", read_pci_config_16(0, 1 , 0, 2));

printf("0 7 1 0 = %x\n", read_pci_config_16(0, 7 , 1, 0));
printf("0 7 1 2 = %x\n", read_pci_config_16(0, 7 , 1, 2));
return 0;
}

    推荐阅读