目录

PCIPCIe硬件相关知识

目录

PCI/PCIe硬件相关知识

Linux下PCI驱动实现(硬件篇)

首先要说明一点,由于作者对内核也不是完全熟悉,所以做内核编程时喜欢猜测,即根据对内核的了解去猜测内核会怎么做,文章有部分内核行为是作者自己猜测,如有错误,欢迎批评指正。

1.PCI历史

PCI协议经历

ISA (Industry Standard Architecture)

MCA (Micro Channel Architecture)

EISA (Extended Industry Standard Architecture)

VLB (VESA Local Bus)

PCI (Peripheral Component Interconnect)

PCI-X (Peripheral Component Interconnect eXtended)

AGP (Accelerated Graphics Port)

PCI Express (Peripheral Component Interconnect Express)

发展史,技术发展是为了解决当前问题,这里的问题就是传输速度,盗用知乎上老狼的一张图

https://i-blog.csdnimg.cn/blog_migrate/cb455a51be0a6d11909c054f7fe17daf.png

可以看到主频的增加促进了传输速度的增加,主频的增加会带来另一个问题,那就是相互间的干扰,PCI设备是并行通信,频率增加,各条线路不可避免就会互相干扰,于是PCIe诞生了,PCIe是串行通信。关于PCIe在以后的章节介绍,本篇文章只考虑PCI协议。

2.PCI总线

一颗典型的PCI总线树如图(还是盗用老狼的图):

https://i-blog.csdnimg.cn/blog_migrate/46c097682129d7c2c9f62ee0b546c96c.png

熟悉总线模型的同学应该都了解这种拓朴结构,及一个根总线,总线挂接设备或者子总线,当然子总线需要桥(bridge)连接。这种结构限制我们在总线上找一个设备时,需要指定总线号及设备号。

3.PCI协议简介

PCI有三种地址空间:I/O空间、内存空间、配置空间。其中I/O空间和内存空间由设备驱动使用,也就是驱动可以对这两类空间进行数据读写。而配置空间由Linux PCI初始化代码使用。

系统启动之后调用PCI初始化,读取配置空间,然后初始化PCI设备,具体的配置空间如下,

https://i-blog.csdnimg.cn/blog_migrate/bd9de18d759dd416fbb7cd21d0602a47.png

配置空间共有64byte大小,Vender ID是厂家ID,这个编号需要向PCI SIG组织申请当然如果自己做也可以,只是不被国际认可,Device ID是厂家自定义的设备号,这两个寄存器的值会在设备枚举过程中被内核抓取,并以此来甄选驱动;Status,设备状态字,百度之后并没有资料详细介绍每一个bit的作用,查找Linux源码,找到如下字段:

#define PCI_STATUS      0x06    /* 16 bits */
#define  PCI_STATUS_INTERRUPT   0x08    /* Interrupt status */
#define  PCI_STATUS_CAP_LIST    0x10    /* Support Capability List */
#define  PCI_STATUS_66MHZ   0x20    /* Support 66 MHz PCI 2.1 bus */
#define  PCI_STATUS_UDF     0x40    /* Support User Definable Features [obsolete] */
#define  PCI_STATUS_FAST_BACK   0x80    /* Accept fast-back to back */
#define  PCI_STATUS_PARITY  0x100   /* Detected parity error */
#define  PCI_STATUS_DEVSEL_MASK 0x600   /* DEVSEL timing */
#define  PCI_STATUS_DEVSEL_FAST     0x000
#define  PCI_STATUS_DEVSEL_MEDIUM   0x200
#define  PCI_STATUS_DEVSEL_SLOW     0x400
#define  PCI_STATUS_SIG_TARGET_ABORT    0x800 /* Set on target abort */
#define  PCI_STATUS_REC_TARGET_ABORT    0x1000 /* Master ack of " */
#define  PCI_STATUS_REC_MASTER_ABORT    0x2000 /* Set on master abort */
#define  PCI_STATUS_SIG_SYSTEM_ERROR    0x4000 /* Set when we drive SERR */
#define  PCI_STATUS_DETECTED_PARITY 0x8000 /* Set on parity error */

源码里的解释一目了然,这里就不多说了。HeaderType表示设备类型是PCI设备还是PCI桥。下面详细说明一下Base Address Registers简称BAR。

Base Address Registers:有三种功能,1.决定PCI设备映射方式,分别是IO和Memory映射。2.决定映射空间大小。3.存储映射基地址。

内存映射如图:

https://i-blog.csdnimg.cn/blog_migrate/04b90a53b9fece7845492f4d6c76ffb4.png

bit0为只读,且值为0,

bit2-bit1:00表示映射成32位地址,10表示映射成64位地址。

bit3:表示是否支持预读,1:支持 0:不支持

I/O映射如下图,

https://i-blog.csdnimg.cn/blog_migrate/122f617f2f901cc97caca6a2ee220251.png

Bit0:为只读,且值为1,

Bit1:Reserved

当系统上电初始化之后,步骤如下

  1. 给BAR全部写1,这样具有可写属性的位被置1。

  2. 读出BAR的值,并clear 上图中特殊编码的值,(IO 中bit0,bit1, memory中bit0-3)。

  3. 将读出来的值加1,得到的值就是内存空间大小

网上搜到的说法是,如果需要1MB的空间,就设置为高12位可读写,bit20-4为只读且为0。

按照我自己的理解,高12位 + 只读位(20-4+1) + 特殊位(4位) = 33,也就是网上说法是错误的,不可能有33位2进制位,正确的设置应该是:

高12位 + 只读位(19-4+1) + 特殊位(4位),按照规则进行位运算正好是1MB。

BAR的第三个功能,存储基地址,当内核读取到所有PCI的BAR需要的地址大小之后,动态分配映射地址,并将地址写入BAR寄存器,读取BAR寄存器的值即是数据地址。

那么如何读取BAR寄存器的值呢,在i386中只需要往寄存器0xCF8中写地址,再去0xCFC中读数据即可。如何写呢,盗用别人的一张图如下:

https://i-blog.csdnimg.cn/blog_migrate/fe4c8e09c2f2ee4ec677fc4231bb70b0.png

bit31置1,bit30-bit24:保留,bit23-bit16:即第2节说的总线号,bit15-bit11:总线分配给具体设备的设备号,这样就找到的具体的物理设备,bit10-bit8:逻辑号,也就是说一个物理设备可以有多个逻辑设备,及一个物理设备有多个功能(一般都将此设置为0),bit7-bit2:寄存器号,这样就找到了我们需要读取的BAR。

那总线号和设备号怎么得到呢,这个在linux系统里会保存到代表具体设备的结构体变量里。

到这里作者还有个疑问,因为做过数字电路设计的人都知道,所谓通信无非就是数据线,地址线,控制线三类,然后先控制信号,然后地址信号,再数据信号,这是现代总线通信的基本模型。在PCI通信里这个模型怎么实现的呢。先看一张引脚连接图:

https://i-blog.csdnimg.cn/blog_migrate/32716e161ef8cf5b289fb27283e96ca0.png

主要看左边,右边是64位扩展,有兴趣的同学可以了解下,有32位数据地址总线AD[31:00],4位控制总线C/BE[3::0],还有一些Interface Control控制线。

来看看写时序图:

https://i-blog.csdnimg.cn/blog_migrate/e62dfc3f332e1a7e6fe2857316df3cac.png

首先了解一下命令控制脚C/BE# :

https://i-blog.csdnimg.cn/blog_migrate/c95b8495967cabb0a9b4cf122187d9d1.png