来源: http://www.ssdfans.com/?p=3644
每个PCIe设备,有这么一段空间,Host软件可以读取它获得该设备的一些信息,也可以通过它来配置该设备,这段空间就叫做PCIe的配置空间。不同于每个设备的其它空间,PCIe设备的配置空间是协议规定好的,哪个地方放什么内容,都是有定义的。PCI或者PCI-X时代就有配置空间的概念,那时的配置空间如下:
整个配置空间就是一系列寄存器的集合,其中Type 0是Endpoint的配置,Type 1是Bridge(PCIe时代就是Switch)的配置,都由两部分组成:64 Bytes的Header+192Bytes的Capability结构,后者是设备告诉Host它有多牛逼,都会什么绝活。
进入PCIe时代,PCIe能耐更大,192 Bytes不足以罗列它的绝活。为了保持后向兼容,又要不把绝活落下,怎么办?很简单,我扩展后者的空间,整个配置空间由256 Bytes扩展成4KB,前面256 Bytes保持不变:
PCIe有什么能耐(Capability)我们不看,我们先挑软柿子捏,先看看只占64 Bytes的Configuration Header。
像Device ID,Vendor ID,Class Code和Revision ID,是只读寄存器,PCIe设备通过这些寄存器告诉Host软件,这是哪个厂家的设备、设备ID是多少、以及是什么类型的(网卡?显卡?桥?)设备。
其它的我们暂时不看,我们看看重要的BAR(Base Address Register)。
对Endpoint Configuration(Type 0),提供了最多6个BAR,而对Switch(Type 1)来说,只有2个。BAR是干什么用的?
每个PCIe设备,都有自己的内部空间,这部分空间如果开放给Host(软件或者CPU)访问,那么Host怎样才能往这部分空间写入数据,或者读数据呢?
我们知道,CPU只能直接访问Host内存(Memory)空间(或者IO空间,我们不考虑),不对PCIe等外设直接操作。怎么办?记得皇帝身边那个有根的太监吗?Root Complex,RC。RC可以为CPU分忧。
解决办法是:CPU如果想访问某个设备的空间,由于它不能(或者不屑)亲自跟那些PCIe外设打交道,因此叫太监RC去办。比如,如果CPU想读PCIe外设的数据,先叫RC通过TLP把数据从PCIe外设读到Host内存,然后CPU从Host内存读数据;如果CPU要往外设写数据,则先把数据在内存中准备好,然后叫RC通过TLP写入到PCIe设备。完美!
上图例子中,最左边虚线的表示CPU要读Endpoint A的数据,RC则通过TLP(经历Switch)数据交互获得数据,并把它写入到系统内存中,然后CPU从内存中读取数据(紫色箭头所示),从而CPU间接完成对PCIe设备数据的读取。
具体实现就是上电的时候,系统把PCIe设备开放的空间(系统软件可见)映射到内存空间,CPU要访问该PCIe设备空间,只需访问对应的内存空间。RC检查该内存地址,如果发现该内存空间地址是某个PCIe设备空间的映射,就会触发其产生TLP,去访问对应的PCIe设备,读取或者写入PCIe设备。
一个PCIe设备,可能有若干个内部空间(属性可能不一样,比如有些可预读,有些不可预读)需要映射到内存空间,设备出厂时,这些空间的大小和属性都写在Configuration BAR寄存器里面,然后上电后,系统软件读取这些BAR,分别为其分配对应的系统内存空间,并把相应的内存基地址写回到BAR。(BAR的地址其实是PCI总线域的地址,CPU访问的是存储器域的地址,CPU访问PCIe设备时,需要把总线域地址转换成存储器域的地址。)
如上图例子,一个Native PCIe Endpoint,只支持Memory Map,它有两个不同属性的内部空间要开放给系统软件,因此,它可以分别映射到系统内存空间的两个地方;还有一个Legacy Endpoint,它既支持Memory Map,还支持IO Map,它也有两个不同属性的内部空间,分别映射到系统内存空间和IO空间。
来个例子,看一下一个PCIe设备,系统软件是如何为其分配映射空间的。
上电时,系统软件首先会读取PCIe设备的BAR0,得到数据:
然后系统软件往该BAR0写入全1,得到:
BAR寄存器有些bit是只读的,是PCIe设备在出厂前就固定好的bit,写全1进去,如果值保持不变,就说明这些bit是厂家固化好的,这些固化好的bit提供了这块内部空间的一些信息:
怎么解读?低12没变,表明该设备空间大小是4KB(2的12次方),然后低4位表明了该存储空间的一些属性(IO映射还是内存映射,32bit地址还是64bit地址,能否预取?做过单片机的人可能知道,有些寄存器只要一读,数据就会清掉,因此,对这样的空间,是不能预读的,因为预读会改变原来的值),这些都是PCIe设备在出厂前都设置好的,提供给系统软件的信息。
然后系统软件根据这些信息,在系统内存空间找到这样一块地方来映射这4KB的空间,把分配的基地址写入到BAR0:
从而最终完成了该PCIe空间的映射。一个PCIe设备可能有若干个内部空间需要开放出来,系统软件依次读取BAR1,BAR2。。。,直到BAR5,完成所有内部空间的映射。
上面主要讲了Endpoint的BAR,Switch也有两个BAR,今天不打算讲,下节讲TLP路由,再回过头来讲。继续说配置空间。
前面说每个PCIe设备都有一个配置空间,其实这样说是不准确的,而是每个PCIe设备至少有一个配置空间。一个PCIe设备,它可能具有多个功能(function),比如既能当硬盘,还能当网卡。每个功能对应一个配置空间。
在一个PCIe拓扑结构里,一条总线下面可以挂几个设备,而每个设备可以具有几个功能,如下所示:
因此,在整个PCIe系统中,只要知道了Bus+Device+Function,就能找到对应的Function。寻址基本单元是功能(function),它的ID就由Bus+Device+Function组成 (BDF)。一个PCIe系统,可以最多有256条Bus,每条Bus上可以挂最多32个Device,而每个Device最多又能实现8个Function,而每个Function对应着4KB的配置空间。上电的时候,这些配置空间都是需要映射到Host的内存空间,因此,需要占用内存空间是:256*32*8*4KB =256MB。在这个动辄4GB、8GB内存的时代,256MB算不了什么。
系统软件是如何读取Configuration空间呢?不能通过BAR中的地址,为什么?别忘了BAR是在Configuration中的,你首先要读取Configuration,才能得到BAR。前面不是系统为所有可能的Configuration预留了256MB内存空间吗?系统软件想访问哪个Configuration,只需指定相应Function对应的内存空间地址,RC发现这个地址是Configuration映射空间,就会产生相应的Configuration Read TLP去获得相应Function的Configuration。
再回想一下前面介绍的Configuration Read TLP的Header格式:
Bus Number + Device + Function就唯一决定了目标设备; Ext Reg Number + Register Number相当于配置空间的偏移。找到了设备,然后指定了配置空间的偏移,就能找到具体想访问的配置空间的某个位置。
结束前,强调一下,只有RC才能发起Configuration的访问请求,其他设备是不允许对别的设备进行Configuration读写的。
09/23/2017 Sat