从cpu角度理解PCIe
1 概述
为什么需要写这篇文章,当我阅读《深入浅出SSD》这篇书籍中PCIe章节时发现,本书籍的侧重点是放在PCIe控制器和PCIe协议上,从CPU角度理解PCIe知识偏少,本文对下面几个知识点做出一些补充。
1. CPU访问外设寄存器与内存编址方式;
2. CPU如何访问PCIe配置空间;
3. CPU能够通过寄存器访问配置空间,为什么还需要映射PCIe配置空间;
4. 如何扫描PCIe树并且为PCIe分配ID;
5. 如何将pcie域地址映射到存储器域地址空间。
通过本篇文章将对问题1、2、3做出解答。
2 统一编址于独立编址
CPU编址是程序指令与物理地址线建立链接的方式,在CPU内部有专门的地址集合,编址过程是由CPU体系架构所决定的,参考示意图如图 1所示(仅仅代表示意图,讲解一种逻辑结构,不代表实际电路)。CPU编址时就已经指定了0x8000_0000~0xFFFF_FFFF这个地址空间为连接到图中内存的地址线,内存如何连接到CPU需要当参考CPU的datasheet,当CPU程序指令对0x8000_0000这个物理地址地址发起访问时,等价于是在访问图中2G内存的首地址。
图 1
内存通过CPU地址总线来寻址定位,然后通过CPU数据总线读写数据。CPU的地址总线位数是CPU设计时确定,因此一款CPU所能寻址的地址范围是一定的,而内存是需要占用CPU的寻址空间的,内存与CPU采用总线直接连接。
IO指的是与CPU连接的各种外设,CPU访问各种外设有两种方式:一种是类似于访问内存的方式,即把外设的寄存器当成内存地址读写,从文可以以访问内存方式操作外设寄存器。这时,IO与内存统一编址,IO地址与内存地址在同一个地址空间下,这种编址方式叫做IO与内存统一编址。另外一种编址方式是IO地址与内存地址分开独立编址,这种编址方式叫做独立编址,此时,CPU访问外设寄存器需要通过CPU特定的指令去访问外设寄存器,而不能通过地址直接访问外设寄存器。常见的ARM、PowerPc、MIPS架构都是采用统一编址,X86架构采用独立编址。
3 访问PCIe配置空间256bytes
PCI总线规定访问配置空间总线事务,使用ID号进行寻址。PCI设备ID号由总线号(Bus Number)、设备号(Device Number)和功能号(Function Number)。其中总线号在HOST主桥遍历PCI总线树时确定,在一颗PCI总线树上,总线号由系统软件决定,通常与HOST主桥直接相连接的PCI总线编号为0,系统软件使用DFS(Depth-First Search)算法扫描PCI总线树上的所有PCI总线,并依次编号。一条PCI总线的设备号由PCI设备的IDSEL信号与PCI总线地址线的连接关系确定,功能号与PCI设备的具体设计有关。一个PCIe系统最多有256条Bus,每条Bus上最多可以挂在32个设备,每个PCIe设备最多有8个功能设备。
在XX处理器中的HOST主桥中,与PCIE设备配置相关的寄存器由CFG_ADDR、CFG_DATA等组成。系统软件使用CFG_ADDR(CFG_ADDR寄存器结构如图 2所示)和CFG_DATA寄存器访问PCIe设备的配置空间,这些寄存器都是采取同一编址(所有内存寄存器都使用存储器映射方式进行寻址)。当处理器访问PCIe配置空间时,首先需要在CFG_ADD寄存器中设置这个PCIe设备对应的总线号、设备号、功能号和寄存器偏移,然后使能Enable位,之后当处理器对CFG_DATA读写访问时,HOST主桥将这个存储器读写访问转换成PCIe配置读写请求,并且发送到PCIe总线上。如果Enable位没有使能,那么CPU对寄存器的访问也就是一个普通IO的访问,而不能让HOST转换成总线请求访问,访问PCIe配置空间时按照PCIe总线标准配置TLP请求,CFG_DATA是读取的数据或者待写入的数据。
图 2
a) 31位:Enable位,为1时,对CFG_DATA读写才能转换成PCIe总线配置请求。
b) 30~24位:保留。
c) 23~16位:总线号,最多=256个。
d) 15~11位:设备号,最多=32个。
e) 10~8位:功能号,最多=8个。
f) 7~2位:寄存器偏移,最多访问寄存器=64个地址,这里一个地址是DW,那么能干访问的PCIe配置空间大小为64*4=256Byte,所以访问PCIe配置空间都是以4字节对齐访问的。
走到这里很多读者可能就会有这样的疑问,既然CPU能够直接通过寄存器访问配置空间,为啥还会出现配置空间在存储域地址的映射这一说法呢?下面给出详细解答。
访问PCIe配置空间寄存器的方法需要追溯到原始的PCI规范。为了发起PCI总线配置周期,Intel(Intel是PCIe龙头老大,最新的PCIe的规范总是它最先尝试的)实现的PCI规范使用IO空间的CF8h和CFCh来分别作为索引和数据寄存器,这种方法可以访问所有PCI设备的255 bytes配置寄存器。Intel Chipsets目前仍然支持这种访PCI配置空间的方法。PCIe规范在PCI规范的基础上,将配置空间扩展到4K bytes,至于为什么扩展到4K,具体可以参考PCIe规范,这些配置CFG_ADDR和CFG_DATA寄存器方法仍然可以访问所有PCIe设备配置空间的头255 bytes,但是该方法访问不了剩下的(255B~4K)配置空间。怎么办呢?Intel外一种PCIe配置空间访问方法。Intel Chipset通过将配置空间映射到内存地址空间,PCIe配置空间可以像对映射范围内的内存进行read/write来访问了。这种映射是由北桥芯片来完成的,但是不同芯片的映射方式也是不同的。目前我查看了ARM芯片的datasheet,确实是这样的方式。
PCIe规范为每个PCIe设备添加了更多的配置寄存器,空间为4K,尽管CFG_ADDR和CFG_DATA寄存器方法仍然能够访问lower 255 bytes,但是必须提供另外一种方法来访问剩下的(255B~4K)range寄存器。Intel的解决方案是使用了预留256MB内存地址空间,对这段内存的任何访问都会发起PCIe 配置cycle。由于4K的配置空间是directly mapped to memory的,那么PCIe规范必须保证所有的PCIe设备的配置空间占用不同的内存地址,按照PCIe规范,支持最多256个bus,每个Bus支持最多32个PCIe devices,每个device支持最多8个function,也就是说:占用内存的最大值为:256 * 32 * 8 * 4K = 256MB。图 3是ARM Cortex-A9 datasheet内存地址分配局部图。被PCIe配置空间占用的256M内存空间会屏蔽掉DRAM使用该段内存区,这些地址都由CPU出厂时已经固化好了。
图 3
4 PCIe配置空间的内存映射对32bit系统的影响
由于PCIe配置空间占用了256M内存空间,而且该被占用空间对DRAM来说是不可用的,这意味着256M空间消失于系统内存,这在32bit系统中更为明显。比如,在32 bit winxp中(作者目前电脑还是用的XP系统,电脑用了七八年了),理论上可以访问到的内存是4G,如果4G空间都被DRAM给占用,由于PCIe的存在,被PCIe占用的那部分内存空间对OS来说是不可用的,莫名的消失了最多256M内存,其实还有其他外设寄存器需要映射到内存,如果是独立编址就不存在寄存器占用内存。所以在XP系统中实际能够访问DRAM空间最大值为3.2G。64位CPU寻址不存在这个情况,个地址目前来说应该用不完,这里读者需要注意的是CPU有32和64位寻址方式,同样操作系统也有32和64位之分,在Linux系统中主要体现在库文件上。
有些CPU没有直接指定PCIe配置空间的地址范围,需要读取某个寄存器的值BaseAddr,这个值就说PCIe配置寄存器在内存区域映射的基地址。访问PCIe设备配置空间时候需要手动计算访问PCIe配置空间的地址。计算发放如下:
SIZE_PER_FUNC = 4K = 1000h
SIZE_PER_DEVICE = 4K * 8 = 8000h
SIZE_PER_BUS = 4K *8* 32 = 100000h
访问总线号为busNo,设备号为DevNo,功能号为funcNo的offset寄存器的计算公式是:
Memory Address = BaseAddr+ busNo * SIZE_PER_BUS+ devNo * SIZE_PER_DEVICE+ funcNo * SIZE_PER_FUNC+ offset
访问PCIe配置空间就需要通过总线号、设备号、功能号、寄存器偏移进行转换成内存地址。转换函数如图 2所示。
图 4
5 地址概述
上电扫描PCIe树和存储地址到PCIe地址的映射,本文可能会针对某一款芯片做出详细流程解答,读者可以只关注整个流程,具体映射机制和寄存器参考芯片datasheet。上篇文章已经了解到如何访问配置空间,前256Bytes可以通过寄存器方式访问,后面的256B~4k必须通过映射才能访问,映射无非就是把配置空间映射到存储地址空间,或者把PCIe设备空间映射到存储地址空间。下面开始讨论映射关系。
6 地址映射关系
PCIe在存储域地址空间分为三部分,PCIe控制器本身的寄存器、PCIe设备的配置空间、PCIe设备空间。寄存器和配置空间由处理器本身决定存储地址范围,本款处理器地址范围如图5所示,配置空间地址、寄存器地址、内存地址都已经确定。PCIe设备空间需要编程人员去配置Outbound和Inbound寄存器组,确定映射关系
图 5
Outbound在PCIe控制器中扮演的角色是将存储地址翻译到PCIe域的PCIe地址,Inbound是将PCIe地址翻译成存储地址,图6是一个完整的RC和EP模型地址翻译模型,图中的地址数字仅仅代表一种形态,具体地址应该是什么在后文中讲解。当cpu需要访问EP的内存空间时,首先应该将存储地址转换成PCIe地址,在根据TLP到达指定的EP,进而将PCIe地址转换成EP端的存储地址。
图 6
PCIe地址到存储地址之间的映射关系由三个寄存器决定(有两个寄存器组应该是32个寄存器)OB_SIZE、OB_OFFSET_INDEXn、OB_OFFSETn_HI,n的范围是0~31。在PCIe控制器中是把PCIe地址等分成32块regions (Regions 0 to 31),每个regions的大小是可以通过编程设置OB_SIZE寄存器确定大小,大小有1, 2, 4, or 8 MB,那么通过Outbound能够翻译的地址最大为8M*32=256M。存储域地址中有5位作为识别32个regions的index,OB_SIZE的大小决定这5位在32位地址上的位置。当OB_SIZE等于0,1,2,3时,index在存储地址中对应的位置是Bits[24:20], bits[25:21], bits[26:22], and bits[27:23],每个regions翻倍,是不是对应的地址应该按翻倍对齐呢,翻倍就是左移一位数据。OB_SIZE寄存器如图7所示。
图 7
OB_OFFSET_INDEXn寄存器结构如图8所示,n是上一段落提到的index的值。该寄存器第0位是地址翻译使能位,第31~20位是第n个regions的基地址的31~20位,这里的取值取决regions的大小,当OB_SIZE 等于0,1,2,3时,bits[31:20], bits[31:21], bits[31:22], and bits[31:23]位相应被使用。OB_OFFSETn_HI寄存器的值是64位PCIe地址中第n个regions的基地址的63~32位,在32位PCIe地址中,该寄存器的值等于0。
图 8
配置OutBound翻译的几个寄存器也做了详解,下面根据举例说明。图9中配置空间存储地址由CPU本身架构所决定,这部分的地址映射才芯片内部完成,不需要由编程人员配置。PCIe设备空间被分成了32等分。假设region大小是2M,PCIe地址是64位,程序中需要对0x9D3A_1234存储地址做映射, 64位PCIe地址被使用在region 9上,初始化OBOFFSET9_HI值为0x3344 5566, OB_OFFSET9值56Ex xxxx(x的值这里不关心,看该寄存器结构就很清楚,第0位在地址翻译时候应该使能位1,这里仅仅用来讲解怎么做映射,不需要关心后面的Bits) ,下面分析怎么翻译到PCIe地址:
1) 由于是regions大小2M,那么index应该取地址的bits [25:21],提取0x9D3A_1234存储地址的bits [25:21得到01001b,该值等于9,那么该地址应该启用regions 9 翻译。存储地址的bits[20:00]是用做翻译到PCIe地址的bits[20:00]位,该值也可以理解成reginos 9内的偏移值,值是0x001A 1234。
2) 生成regions 块PCIe的基地址,该地址应pcie_base=OBOFFSET9_HI <<32 + OB_OFFSET9的bits[31:21] = 0x 3344 5566 56E0 0000。
3) 计算PCIe地址,pcie_addr = pcie_base + 存储地址bits[20:0] =0x3344 5566 56FA 1234。
图 9
从上面存储地址到PCIe地址映射可以看到,通过cpu寻址可以直接访问到PCIe设备空间,最多可以访问PCIe设备空间大小为256M,具体Outbound能够访问的大小根据芯片而定,当CPU与FPGA之间有大量数据交互时候也可以采用Inbound方式(Inbound地址翻译流程如图10所示,这里就不在翻译),将CPU的内存映射到FPGA的寻址空间(这里是站在CPU角度看的,从图6可以理解具体映射大小还由EP决定),FPGA可以采用DMA方式访问cpu的内存,并且速度很快。有些芯片厂商干脆采用同核异构方式将CPU于FPGA集成在一起(有的将cpu与dsp集成在一起),两者之间采用AXI高速总线通讯。
图 10
7 扫描PCIe树
扫描树的流程如下:
1) 建立存储地址到PCIe地址映射 (映射方式上面段落已经讲解了,固定的PCIe配置空间映射)
2) 分配PCIe总线号
3) 分配设备号
4) 访问配置空间 (这里有一个原则读者需要注意,对PCIe设备配置空间访问时,一定要确定总线号、设备号、功能号、寄存器,不然无法找到设备)
5) 读写BAR0确定PCIe设备1空间大小
6) 分配PCIe地址
7.1 分配总线号
扫描PCIe总线树时,需要对这些PCIe总线进行编号,即初始化PCIe桥(在本文一律指透明桥)的Primary、Secondary和Subordinate Bus寄存器。在Linux内核中采用DFS算法对PCIe总线树进行遍历,DFS算法是按照深度优先的原则遍历PCIe树,局部代码如图11所示,这里可以跟踪pci_scan_bridge函数,函数采用DFS算法对总线进行编号(后期会讲解搜索树,比较常见hash表、红黑树)。
图 11
7.2 分配设备号
PCIe设备的IDSEL信号与PCIe总线的AD[31:0]信号的连接关系决定了该设备在这条总线上的设备号。在配置读写总线事务的地址周期中,AD[10:0]已经被信号已经被功能号和寄存器号使用,因此PCIe设备的IDSEL只能与AD[31:11]信号连接。上一篇文章中谈到CONFIG_ADDR寄存器中的Device Number字段一共有5位,最大能够表示32个设备,这里只有21位,显然在两者之间不能建立一一映射关系。一个PCIe总线号下最多可以挂在21个PCIe设备,那么多个PCIe总线不就可以挂载32个设备了么。
7.3 访问配置空间
在32位PCIe地址空间中,PCIe设备通常将PCIe配置存放在E2PROM中,PCIe设备进行上电初始化时,将E2PROM中的信息读到PCIe设备的配置空间作为初始值,由硬件自动完成。BAR0空间存储了PCIe设备空间的大小,某些位被设置成不可预读,当BAR0全部写入1时,然后在读取BAR0值,从数据低位看有多少连续位没有改变。没有改变的数据位数记录的该PCIe设备空间的大小,假如有n位没有改变,那么设备空间大小应该是2的n次方。第0位代表IO/Memory、第2,3位代表32/64位地址、第4位代表是否可预取,具体位定义格式可以直接参考内核PCIe总线代码,解析BAR函数如图12所示。
图 12
7.4 分配PCIe地址
系统软件根据根据设备空间大小建立存储地址PCIe设备地址空间的映射,给PCIe设备分配的PCIe基地址写入到BAR0,如果是64位PCIe地址,那么BAR1是高32位地址。
8 结语
写《从cpu角度理解PCIe》文章我参考了部分芯片的datasheet,并结合linux代码分析,本文仅仅起到分析流程的作用,具体映射机制和寄存器芯片参考相应芯片datasheet。如有错误,还望指正。
- 点赞
- 收藏
- 关注作者
评论(0)