本篇文章分析PCIe上电是如何枚举的,BAR空间访问在以前文章已经讲解,可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。本文篇幅较长,读者需要耐心阅读。

        PCIe上电枚举算法主要是分配总线号和分配BAR空间,分配的总线号用于访问配置空间,BAR空间用于与PCIe设备进行数据交互。PCIe协议包括物理层,链路层和事务层,本次分析从事务层开始着手,事务层数据交互的单元是TLP包,TLP有公共的头部和载荷数据,载荷数据最大为4KB,根据公共头部TLP可以分为Memory Read/Write ,Configure Type0 Read/Write Request , Configure Type1 Read/Write Request,Message Request和Completion。上电枚举主要通道配置Type0和Type1类型数据包,Mem数据类型用于BAR空间数据交互。

        PCIe总线定义了两类配置请求,一个是 Type0配置请求,另一个是 Type1配置请求。 PCIe总线使用这些配置请求访问 PCIe总线树上的设备配置空间,包括PCIe桥和 PCIe设备的配置空间,PCIe总线使用Type0访问PCIe设备配置空间,使用Type1访问PCIe桥配置空间,在桥下面的PCIe设备桥会自动将Type1请求转化成Type0请求,如果桥下面还是桥就将Type1请求向下传递。

        在PCIe Host中,HOST主桥根据该寄存器的 Bus Number字段,决定是产生 Type0配置请求,还是Type 1配置请求。 当 Bus Number字段为0时,将产生 Type0配置请求,因为与 HOST主桥直接相连的总线号为 0;大于0时,将产生Type 01h配置请求.配置请求TLP包格式如下所示(图片不是很清晰,借用网络上的图片),我们主要关心Bus Num这一行的信息,里面有总线号,设备号,功能号,寄存器偏移等。

         前面提到PCIe桥会自动将type1请求转化成Type0请求或者向下传递Type1请求,这里描述下具体规则。PCIe桥配置空间如下所示,配置空间有三个重要的寄存器Primary Bus Number,Secondary Bus Number,Subordinate Bus Number。这三个寄存器用于确定PCIe总线号的唯一性,如何访问配置空间的总线号为0,表示访问设备直接挂在HOST上,如果总线号大于>0, Host发起Type1请求,PCIe桥会根据Type1配置请求的总线号判断,如果总线号等于PCIe桥的Secondary Bus Number值,PCIe桥就将Type1请求转化成Type0请求,发送给PCIe桥下的设备(这里需要注意,PCIe桥是不接收Type0请求的),如果总线大于Secondary Bus Numbe值,表明本次请求访问在PCIe桥的下一级桥下面,以此类似实现配置请求转化规则访问。

        配置请求转换规则上面已经讲得比较清楚了,如果不清楚的从文章开始仔细阅读,反复阅读加深理解。上面部分多次提到总线号,但是总线号怎么分配呢,我以下面的PCIe拓扑图来讲解(图片来源于网络,不影响具体分析)。

        Linux驱动枚举采用的时DFS算法,在文章后半部分讲解代码实现。文以上图为例(图片中的PCI更换成PCIe,懒得再画图就从网上找的图片),说明系统软件如何使用DFS算法,分配PCIe总线号,并初始化 PCIe桥中的Primary Bus Number、 SecondaryBus Number和 SubordinateBus number寄存器。所谓 DFS算法是指按照深度优先的原则遍历PCIe树,访问时采用Type1或者Type0配置请求,总线号最开始填写0,只有第一步采用的时Type0配置请求,因为HOST直接相连部分总线号为0,具体枚举骤如下。

  1. HOST主桥扫描PCIe总线 0上的设备。系统软件首先忽略所有这条总线上的 PCIe设备,因为在这些设备之下不会挂接新的 PCIe总线。例如 PCIe设备 01下不可能挂接新的 PCIe总线。
  2. HOST主桥首先发现PCIe桥1,并将 PCIe桥 1的 SecondaryBus命名为PCIe总线1。 系统软件将初始化 PCIe桥1的配置空间,将 PCIe桥1的 PrimaryBus Number寄存器赋值为 0,而将 Secondary Bus Number寄存器赋值为1,即 PCIe桥 1的上游 PCIe总线号为 0,而下游PCIe总线号为1。
  3.  扫描 PCIe总线 1,发现 PCIe桥2,并将 PCIe桥 2的 SecondaryBus命名为PCIe总线2。系统软件将初始化PCIe桥 2的配置空间,将 PCIe桥 2的 Primary Bus Number寄存器赋值为 1,而将SecondaryBusNumber寄存器赋值为2。
  4. 扫描 PCIe总线 2,发现 PCIe桥3,并将 PCIe桥 3的 SecondaryBus命名为PCIe总线3。系统软件将初始化PCIe桥 3的配置空间,将 PCIe桥 3的 Primary Bus Number寄存器赋值为 2,而将SecondaryBusNumber寄存器赋值为3
  5. 扫描PCIe总线3,没有发现任何PCIe桥,这表示PCIe总线3下不可能有新的总线,此时系统软件将PCIe桥3的SubordinateBusnumber寄存器赋值为3。系统软件在完成PCIe总线的扫描后,将回退到PCIe总线3的上一级总线,即PCIe总线2,继续进行扫描。
  6. 在重新扫描PCIe总线2时, 系统软件发现PCIe总线2上除了PCIe桥3之外没有发现新的PCIe桥,而PCIe桥 3之下的所有设备已经完成了扫描过程,此时系统软件将 PCIe桥2的 SubordinateBusnumber寄存器赋值为3。继续回退到PCIe总线1。
  7.  PCIe总线1上除了PCIe桥2外, 没有其他桥片, 于是继续回退到PCIe总线0, 并将PCIe桥 1的Subordinate Busnumber寄存器赋值为3。
  8. 在PCIe总线0上,系统软件扫描到PCIe桥4,则首先将PCIe桥4的PrimaryBusNumber寄存器赋值为0,而将SecondaryBusNumber寄存器赋值为4,即PCIe桥1的上游PCIe总线号为0,而下游PCIe总线号为4。
  9. 系统软件发现PCIe总线4上没有任何PCIe桥,将结束对PCIe总线4的扫描,并将PCIe桥 4的SubordinateBusnumber寄存器赋值为4, 之后回退到PCIe总线4的上游总线, 即PCIe总线0继续进行扫描。
  10. 系统软件发现在PCIe总线0上的两个桥片PCIe总线0和PCIe总线4都已完成扫描后,将结束对PCIe

        到此为止PCIe树下面的所有配置空间就算访问完了,细心的读者读到这里会不会有这样的疑问,设备号怎么确定的,功能号是具体设备可以确定,在PCI协议中设备号是由IDSEL信号线决定,每一个PCI设备都使用独立的IDSEL信号,这样就能够唯一确定设备号,在PCIe总线中,设备号由switch 的端口确定,因为每一级总线switch的端口编号是唯一的,所有设备号也就唯一确定了,交换机设备号确定规则如下图所示,下联口由交换机端口决定,上联口由上一级设备如何连接相关,下联口总现号为上游口总现号+1。软件枚举时候需要遍历256个槽位(每一级总线最多挂载32个设备,每个设备的8个function),这个后期代码可以看到。软件已经可以访问PCIe树种所有配置空间了,访问Bar空间细节可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。

 

下面分析下ZYNQ的地址转换规则,zynq type类型转换规则如下所示。        

        当访问某个地址段时,PCIe IP会将该地址转换成配置TLP请求,地址构成如下图所示,基地址为AXI访问PCIe配置寄存器的地址,访问配置空在加上下面这个就行可以自动转化成Type0或者Type1请求,具体哪个请求需要看Bus Number是否等于0。

        ZYNQ的BAR空间采用了地址线性映射,与PowerPc的outbound/inbound基本类似,ZYNQ中有6个BAR空间,使用12个地址进行配置。具体看xilinx手册,手册部分知识点如下。

        终于来到代码环节了,撸代码是幸福的,特别是撸linux内核代码。我们直接进入正题,看代码时需要看软件怎么访问配置空间的。内核提供的通用访问配置空间APIpci_bus_read_config_dword,这个函数定义规则如下。

        代码内部调用了ops->read,ops结构定义如下所示,read函数又指向了一个linux通用的函数,这里需要看一下下图中的map函数,在pci_generic_config_read函数中会调用这个map_bus函数返回地址,map_bus函数转换规则就是按照xilinx手册中指定的规则,指定总线号和设备号就行。

pci_generic_config_read函数定义如下,看到没有就回到自定义转换规则的map函数了。

        终于为枚举铺平了道路,讲解了TLP如何产生请求,又讲解了软件如何访问配置空间,下面开始真正的枚举算法,直接进入正题分析代码。

        pci_scan_child_bus函数就是枚举函数入口,此时总线号为0,需要枚举256个槽位,也就是设备号,此时PCIe IP产生的时Type0请求,下面遇到相同函数的就直接跳过,不浪费篇幅了。

        pci_scan_slot函数实现如下所示,先判断这个设备function0是否存在,如果不存在就直接返回,如果存在才会去遍历扫描其他7个function,一共8个function 

        当总线0扫描完成,软件需要判断下面设备中是否有switch设备,如果有就按照Typ1配置请求发起访问,判断代码如下所示。

        pci_scan_bridge桥函数如下所示。这里面就有计算前面说的Primary Bus Number,Secondary Bus Number,Subordinate Bus Number三个总线号,计算完成后继续递归调用pci_scan_child_bus函数就实现了枚举。

         代码比较简单,读者可以自行分析代码,这里推荐分析代码的方法,用思维导图分析内核代码比较方便。本文着重讲了如何实现枚举过程的,BAR空间的访问 可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐