全网最全PCIe枚举算法分析(以ZYNQ平台实例讲解)
本篇文章分析PCIe上电是如何枚举的,BAR空间访问在以前文章已经讲解,可以参考《从cpu角度理解PCIe》和《从cpu角度理解PCIe续集》。本文篇幅较长,读者需要耐心阅读。PCIe上电枚举算法主要是分配总线号和分配BAR空间,分配的总线号用于访问配置空间,BAR空间用于与PCIe设备进行数据交互。PCIe协议包括物理层,链路层和事务层,本次分析从事务层开始着手,事务层数据交互的单元是TLP包,
本篇文章分析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,具体枚举骤如下。
- HOST主桥扫描PCIe总线 0上的设备。系统软件首先忽略所有这条总线上的 PCIe设备,因为在这些设备之下不会挂接新的 PCIe总线。例如 PCIe设备 01下不可能挂接新的 PCIe总线。
- 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。
- 扫描 PCIe总线 1,发现 PCIe桥2,并将 PCIe桥 2的 SecondaryBus命名为PCIe总线2。系统软件将初始化PCIe桥 2的配置空间,将 PCIe桥 2的 Primary Bus Number寄存器赋值为 1,而将SecondaryBusNumber寄存器赋值为2。
- 扫描 PCIe总线 2,发现 PCIe桥3,并将 PCIe桥 3的 SecondaryBus命名为PCIe总线3。系统软件将初始化PCIe桥 3的配置空间,将 PCIe桥 3的 Primary Bus Number寄存器赋值为 2,而将SecondaryBusNumber寄存器赋值为3
- 扫描PCIe总线3,没有发现任何PCIe桥,这表示PCIe总线3下不可能有新的总线,此时系统软件将PCIe桥3的SubordinateBusnumber寄存器赋值为3。系统软件在完成PCIe总线的扫描后,将回退到PCIe总线3的上一级总线,即PCIe总线2,继续进行扫描。
- 在重新扫描PCIe总线2时, 系统软件发现PCIe总线2上除了PCIe桥3之外没有发现新的PCIe桥,而PCIe桥 3之下的所有设备已经完成了扫描过程,此时系统软件将 PCIe桥2的 SubordinateBusnumber寄存器赋值为3。继续回退到PCIe总线1。
- PCIe总线1上除了PCIe桥2外, 没有其他桥片, 于是继续回退到PCIe总线0, 并将PCIe桥 1的Subordinate Busnumber寄存器赋值为3。
- 在PCIe总线0上,系统软件扫描到PCIe桥4,则首先将PCIe桥4的PrimaryBusNumber寄存器赋值为0,而将SecondaryBusNumber寄存器赋值为4,即PCIe桥1的上游PCIe总线号为0,而下游PCIe总线号为4。
- 系统软件发现PCIe总线4上没有任何PCIe桥,将结束对PCIe总线4的扫描,并将PCIe桥 4的SubordinateBusnumber寄存器赋值为4, 之后回退到PCIe总线4的上游总线, 即PCIe总线0继续进行扫描。
- 系统软件发现在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内核代码。我们直接进入正题,看代码时需要看软件怎么访问配置空间的。内核提供的通用访问配置空间API为pci_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续集》。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)